@penvhq/cli 0.1.0 → 0.3.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
@@ -26,28 +26,34 @@ __export(index_exports, {
26
26
  insertEnvAlias: () => insertEnvAlias,
27
27
  main: () => main,
28
28
  renderDoctor: () => renderDoctor,
29
+ renderFill: () => renderFill,
29
30
  renderMove: () => renderMove,
31
+ renderPull: () => renderPull,
30
32
  renderPush: () => renderPush,
33
+ renderRotate: () => renderRotate,
31
34
  renderWatch: () => renderWatch,
32
35
  runDecrypt: () => runDecrypt,
33
36
  runDoctor: () => runDoctor,
34
37
  runEncrypt: () => runEncrypt,
35
38
  runExplain: () => runExplain,
39
+ runFill: () => runFill,
36
40
  runGenerate: () => runGenerate,
37
41
  runGet: () => runGet,
38
42
  runInit: () => runInit,
39
43
  runList: () => runList,
40
44
  runMain: () => runMain,
41
45
  runMove: () => runMove,
46
+ runPull: () => runPull,
42
47
  runPush: () => runPush,
43
48
  runRemove: () => runRemove,
49
+ runRotate: () => runRotate,
44
50
  runSet: () => runSet,
45
51
  runValidate: () => runValidate,
46
52
  runWatch: () => runWatch
47
53
  });
48
54
  module.exports = __toCommonJS(index_exports);
49
- var import_core20 = require("@penvhq/core");
50
- var import_citty15 = require("citty");
55
+ var import_core22 = require("@penvhq/core");
56
+ var import_citty18 = require("citty");
51
57
 
52
58
  // src/commands/doctor.ts
53
59
  var import_core7 = require("@penvhq/core");
@@ -55,17 +61,42 @@ var import_sink_github2 = require("@penvhq/sink-github");
55
61
  var import_citty3 = require("citty");
56
62
 
57
63
  // src/project.ts
58
- var import_node_path = require("path");
64
+ var import_node_path2 = require("path");
59
65
  var import_core2 = require("@penvhq/core");
60
66
  var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
61
67
 
62
68
  // src/registry.ts
69
+ var import_node_module = require("module");
70
+ var import_node_path = require("path");
71
+ var import_node_url = require("url");
63
72
  var import_core = require("@penvhq/core");
64
73
  var import_provider_filesystem = require("@penvhq/provider-filesystem");
74
+ var import_provider_kubernetes = require("@penvhq/provider-kubernetes");
75
+ 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
+ var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
65
79
  var LOCAL_TREE_TYPE = "filesystem";
66
80
  var REGISTRY = /* @__PURE__ */ new Map([
67
- [LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })]
81
+ [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
+ [
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") })]
68
89
  ]);
90
+ var CONTRACT_METHODS = [
91
+ "read",
92
+ "write",
93
+ "list",
94
+ "remove",
95
+ "readMeta",
96
+ "writeMeta",
97
+ "removeMeta"
98
+ ];
99
+ var pluginModuleCache = /* @__PURE__ */ new Map();
69
100
  function isProviderRegistered(type) {
70
101
  return REGISTRY.has(type);
71
102
  }
@@ -76,20 +107,102 @@ function createProvider(type, context) {
76
107
  }
77
108
  return factory(context);
78
109
  }
79
- function assertProvidersRegistered(config) {
110
+ async function createSourceProvider(type, context) {
111
+ if (REGISTRY.has(type)) {
112
+ return createProvider(type, context);
113
+ }
114
+ return loadPluginProvider(type, context);
115
+ }
116
+ async function loadPluginProvider(type, context) {
117
+ const fromDir = resolutionBase(context);
118
+ const specifier = pluginSpecifier(context.providerConfig, type);
119
+ const resolved = resolvePlugin(specifier, fromDir);
120
+ if (resolved === void 0) {
121
+ throw unknownProvider(type, context.environment, specifier);
122
+ }
123
+ let mod;
124
+ try {
125
+ mod = await importPlugin(resolved);
126
+ } catch {
127
+ throw new import_core.PenvError(
128
+ "PROVIDER_PLUGIN_LOAD",
129
+ `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
130
+ "It resolved but threw while importing. Check it builds and its dependencies are installed."
131
+ );
132
+ }
133
+ const factory = mod[PLUGIN_FACTORY_EXPORT];
134
+ if (typeof factory !== "function") {
135
+ throw new import_core.PenvError(
136
+ "PROVIDER_PLUGIN_INVALID",
137
+ `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
138
+ `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
139
+ );
140
+ }
141
+ const provider = await factory(context);
142
+ assertSatisfiesContract(provider, specifier);
143
+ return provider;
144
+ }
145
+ function assertProvidersRegistered(config, projectRoot) {
80
146
  for (const [environment, provider] of Object.entries(config.providers)) {
81
- if (!isProviderRegistered(provider.type)) {
82
- throw unknownProvider(provider.type, environment);
147
+ if (isProviderRegistered(provider.type)) {
148
+ continue;
149
+ }
150
+ const specifier = pluginSpecifier(provider, provider.type);
151
+ if (resolvePlugin(specifier, projectRoot) === void 0) {
152
+ throw unknownProvider(provider.type, environment, specifier);
153
+ }
154
+ }
155
+ }
156
+ function pluginSpecifier(providerConfig, type) {
157
+ return providerConfig?.module ?? `@penvhq/provider-${type}`;
158
+ }
159
+ function resolutionBase(context) {
160
+ return (0, import_node_path.dirname)(context.root);
161
+ }
162
+ function resolvePlugin(specifier, fromDir) {
163
+ try {
164
+ const require2 = (0, import_node_module.createRequire)((0, import_node_path.resolve)(fromDir, "noop.js"));
165
+ return require2.resolve(specifier);
166
+ } catch {
167
+ return void 0;
168
+ }
169
+ }
170
+ function importPlugin(resolvedPath) {
171
+ const cached2 = pluginModuleCache.get(resolvedPath);
172
+ if (cached2 !== void 0) {
173
+ return cached2;
174
+ }
175
+ const loading = import((0, import_node_url.pathToFileURL)(resolvedPath).href);
176
+ pluginModuleCache.set(resolvedPath, loading);
177
+ return loading;
178
+ }
179
+ function assertSatisfiesContract(provider, specifier) {
180
+ for (const method of CONTRACT_METHODS) {
181
+ if (typeof provider[method] !== "function") {
182
+ throw new import_core.PenvError(
183
+ "PROVIDER_PLUGIN_INVALID",
184
+ `The provider from \`${specifier}\` is missing \`${method}()\``,
185
+ "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
186
+ );
83
187
  }
84
188
  }
85
189
  }
86
- function unknownProvider(type, environment) {
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) {
87
199
  const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
88
200
  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}.`;
89
202
  return new import_core.PenvError(
90
203
  "UNKNOWN_PROVIDER",
91
204
  `The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
92
- `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.`
205
+ remedy
93
206
  );
94
207
  }
95
208
 
@@ -97,9 +210,9 @@ function unknownProvider(type, environment) {
97
210
  var PENV_DIR = ".penv";
98
211
  function openProject(cwd) {
99
212
  const { config, file } = (0, import_core2.loadConfig)(cwd);
100
- const root = (0, import_node_path.dirname)(file);
101
- const penvDir = (0, import_node_path.resolve)(root, PENV_DIR);
102
- assertProvidersRegistered(config);
213
+ const root = (0, import_node_path2.dirname)(file);
214
+ const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
215
+ assertProvidersRegistered(config, root);
103
216
  return {
104
217
  root,
105
218
  configFile: file,
@@ -118,6 +231,18 @@ function localTree(project) {
118
231
  }
119
232
  return project.provider;
120
233
  }
234
+ async function sourceProviderFor(project, environment) {
235
+ const providerConfig = project.config.providers[environment];
236
+ if (providerConfig === void 0) {
237
+ return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
238
+ }
239
+ return createSourceProvider(providerConfig.type, {
240
+ root: project.penvDir,
241
+ config: project.config,
242
+ providerConfig,
243
+ environment
244
+ });
245
+ }
121
246
  function targetEnvironment(project, explicit) {
122
247
  return (0, import_core2.resolveEnvironment)(project.config, explicit);
123
248
  }
@@ -138,6 +263,26 @@ function refFromKey(key, config) {
138
263
  }
139
264
  return { namespace: segments.slice(0, -1), name };
140
265
  }
266
+ function assertWritableKey(key) {
267
+ const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
268
+ if (segments.length === 0 || segments.every((s) => (0, import_core2.isCanonicalSegment)(s))) {
269
+ return;
270
+ }
271
+ const ref = (0, import_core2.refFromAccessPath)(segments);
272
+ if (ref !== void 0) {
273
+ const suggestion = [...ref.namespace, ref.name].join("/");
274
+ throw new import_core2.PenvError(
275
+ "PARAMETER_KEY_CASING",
276
+ `\`${key}\` is not a canonical parameter name`,
277
+ `Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
278
+ );
279
+ }
280
+ throw new import_core2.PenvError(
281
+ "PARAMETER_KEY_UNREACHABLE",
282
+ `No value file can be named that reaches \`${key}\``,
283
+ "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
+ );
285
+ }
141
286
  function keySourceFor(project, environment) {
142
287
  return (0, import_core2.resolveKeySource)(project.config, environment);
143
288
  }
@@ -493,36 +638,36 @@ async function runPush(options) {
493
638
  decrypted: outbound.filter((item) => item.encrypted).length
494
639
  };
495
640
  }
496
- function renderPush(result) {
497
- if (result.pushed === 0) {
641
+ function renderPush(result2) {
642
+ if (result2.pushed === 0) {
498
643
  return formatRows([
499
644
  {
500
645
  glyph: CHECK,
501
646
  label: "Nothing to push",
502
- subject: `no values resolve for environment ${result.environment}`
647
+ subject: `no values resolve for environment ${result2.environment}`
503
648
  }
504
649
  ]);
505
650
  }
506
- const target = `GitHub Actions for environment ${result.environment}${result.repo === void 0 ? "" : ` (${result.repo})`}`;
651
+ const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
507
652
  const rows = [
508
653
  {
509
654
  glyph: CHECK,
510
655
  label: "Pushed",
511
- subject: `${result.pushed} ${result.pushed === 1 ? "secret" : "secrets"}`,
656
+ subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
512
657
  detail: `to ${target}`
513
658
  },
514
659
  {
515
660
  glyph: CHECK,
516
661
  label: "Scopes",
517
- subject: `${result.environmentSecrets} environment, ${result.repositorySecrets} repository`,
662
+ subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
518
663
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
519
664
  }
520
665
  ];
521
- if (result.decrypted > 0) {
666
+ if (result2.decrypted > 0) {
522
667
  rows.push({
523
668
  glyph: WARN,
524
669
  label: "Decrypted",
525
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
670
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
526
671
  detail: "sent as plaintext for GitHub to re-seal under its own key"
527
672
  });
528
673
  }
@@ -542,19 +687,19 @@ var pushCommand = (0, import_citty.defineCommand)({
542
687
  },
543
688
  run({ args }) {
544
689
  return guard(async () => {
545
- const result = await runPush({
690
+ const result2 = await runPush({
546
691
  cwd: process.cwd(),
547
692
  ...args.env === void 0 ? {} : { environment: args.env },
548
693
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
549
694
  });
550
- write(renderPush(result));
695
+ write(renderPush(result2));
551
696
  });
552
697
  }
553
698
  });
554
699
 
555
700
  // src/commands/validate.ts
556
- var import_node_path2 = require("path");
557
- var import_node_url = require("url");
701
+ var import_node_path3 = require("path");
702
+ var import_node_url2 = require("url");
558
703
  var import_core6 = require("@penvhq/core");
559
704
  var import_citty2 = require("citty");
560
705
  var import_jiti = require("jiti");
@@ -623,16 +768,16 @@ function validationIssuesOf(cause, schemaPath) {
623
768
  }
624
769
  var schemaLoads = Promise.resolve();
625
770
  function exclusively(work) {
626
- const result = schemaLoads.then(work, work);
627
- schemaLoads = result.then(
771
+ const result2 = schemaLoads.then(work, work);
772
+ schemaLoads = result2.then(
628
773
  () => void 0,
629
774
  () => void 0
630
775
  );
631
- return result;
776
+ return result2;
632
777
  }
633
778
  function loadSchema(project, environment) {
634
779
  const schemaPath = (0, import_core6.schemaFileOf)(project.config);
635
- const file = (0, import_node_url.pathToFileURL)((0, import_node_path2.resolve)(project.root, schemaPath)).href;
780
+ const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
636
781
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
637
782
  }
638
783
  async function loadSchemaExclusively(file, schemaPath, environment) {
@@ -734,9 +879,9 @@ async function runValidate(options) {
734
879
  }
735
880
  }
736
881
  drift = computeDrift({ schema, resolutions, config: project.config, environment });
737
- const result = schema.safeParse(object);
738
- if (!result.success) {
739
- for (const problem of result.error.issues) {
882
+ const result2 = schema.safeParse(object);
883
+ if (!result2.success) {
884
+ for (const problem of result2.error.issues) {
740
885
  const path = problem.path.join(".");
741
886
  if (undecryptable.has(path)) {
742
887
  continue;
@@ -752,24 +897,24 @@ async function runValidate(options) {
752
897
  }
753
898
  return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
754
899
  }
755
- function renderValidate(result) {
756
- if (result.ok) {
900
+ function renderValidate(result2) {
901
+ if (result2.ok) {
757
902
  return formatRows([
758
903
  {
759
904
  glyph: CHECK,
760
905
  label: "Schema valid",
761
- subject: `${result.parameters} parameters for environment ${result.environment}`
906
+ subject: `${result2.parameters} parameters for environment ${result2.environment}`
762
907
  }
763
908
  ]);
764
909
  }
765
- const rows = result.issues.map((issue) => ({
910
+ const rows = result2.issues.map((issue) => ({
766
911
  glyph: WARN,
767
912
  label: LABELS[issue.kind],
768
913
  subject: issue.subject,
769
914
  detail: issue.message
770
915
  }));
771
916
  const lines = formatRows(rows);
772
- const remedies = [...new Set(result.issues.map((issue) => issue.remedy))];
917
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
773
918
  for (const remedy of remedies) {
774
919
  if (remedy !== void 0) {
775
920
  lines.push(` ${remedy}`);
@@ -787,12 +932,12 @@ var validateCommand = (0, import_citty2.defineCommand)({
787
932
  },
788
933
  run({ args }) {
789
934
  return guard(async () => {
790
- const result = await runValidate({
935
+ const result2 = await runValidate({
791
936
  cwd: process.cwd(),
792
937
  ...args.env === void 0 ? {} : { environment: args.env }
793
938
  });
794
- write(renderValidate(result));
795
- if (!result.ok) {
939
+ write(renderValidate(result2));
940
+ if (!result2.ok) {
796
941
  process.exitCode = 1;
797
942
  }
798
943
  });
@@ -800,6 +945,7 @@ var validateCommand = (0, import_citty2.defineCommand)({
800
945
  });
801
946
 
802
947
  // src/commands/doctor.ts
948
+ var STUCK_THRESHOLD_MS = 864e5;
803
949
  function skipped(check, label) {
804
950
  return {
805
951
  check,
@@ -813,6 +959,8 @@ async function runDoctor(options) {
813
959
  const project = openProject(options.cwd);
814
960
  const environment = targetEnvironment(project, options.environment);
815
961
  const findings = [];
962
+ const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
963
+ const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
816
964
  const { schema, issues } = await loadSchema(project, environment);
817
965
  if (schema !== void 0) {
818
966
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -861,6 +1009,14 @@ async function runDoctor(options) {
861
1009
  findings.push(...plaintextSecretFindings(subjects, environment));
862
1010
  findings.push(...publicSecretFindings(subjects, environment, project.config));
863
1011
  findings.push(...encryptionFindings(subjects, environment));
1012
+ const rotation = await rotationSubjects(project, environment, subjects, options.source);
1013
+ if (rotation.kind === "unreachable") {
1014
+ findings.push(rotation.finding);
1015
+ } else {
1016
+ findings.push(...overdueFindings(rotation.subjects, environment, now));
1017
+ findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
1018
+ }
1019
+ findings.push(...await providerDriftFindings(project, environment, options.source));
864
1020
  findings.push(...await sinkFindings(project, environment, options.sink));
865
1021
  findings.push({
866
1022
  check: "provider",
@@ -1278,6 +1434,246 @@ async function sinkFindings(project, environment, override) {
1278
1434
  });
1279
1435
  return findings;
1280
1436
  }
1437
+ function humanizeMs(ms) {
1438
+ const abs = Math.max(0, ms);
1439
+ const day = 864e5;
1440
+ const hour = 36e5;
1441
+ const minute = 6e4;
1442
+ const round = (value, unit) => {
1443
+ const n = Math.round(value);
1444
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
1445
+ };
1446
+ if (abs >= day) return round(abs / day, "day");
1447
+ if (abs >= hour) return round(abs / hour, "hour");
1448
+ if (abs >= minute) return round(abs / minute, "minute");
1449
+ return round(abs / 1e3, "second");
1450
+ }
1451
+ function refPathOf(ref) {
1452
+ return [...ref.namespace, ref.name].join("/");
1453
+ }
1454
+ async function rotationSubjects(project, environment, local, override) {
1455
+ const providerConfig = project.config.providers[environment];
1456
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
1457
+ return { kind: "read", subjects: local };
1458
+ }
1459
+ const source = override ?? await sourceProviderFor(project, environment);
1460
+ try {
1461
+ const subjects = await Promise.all(
1462
+ local.map(async ({ resolution }) => ({
1463
+ resolution,
1464
+ meta: await source.readMeta(resolution.ref)
1465
+ }))
1466
+ );
1467
+ return { kind: "read", subjects };
1468
+ } catch (error) {
1469
+ return {
1470
+ kind: "unreachable",
1471
+ finding: {
1472
+ check: "rotation-overdue",
1473
+ severity: "unknown",
1474
+ label: "Rotation",
1475
+ subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
1476
+ detail: errorDetail(error)
1477
+ }
1478
+ };
1479
+ }
1480
+ }
1481
+ function overdueFindings(subjects, environment, now) {
1482
+ const findings = [];
1483
+ for (const { resolution, meta } of subjects) {
1484
+ const { policy, lastRotated } = (0, import_core7.rotationOf)(meta, environment);
1485
+ if (policy === void 0 || lastRotated === null) {
1486
+ continue;
1487
+ }
1488
+ const interval = (0, import_core7.tryParseDuration)(policy);
1489
+ if (interval === void 0) {
1490
+ findings.push({
1491
+ check: "rotation-overdue",
1492
+ severity: "warning",
1493
+ label: "Rotation policy invalid",
1494
+ subject: resolution.parameter,
1495
+ detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
1496
+ });
1497
+ continue;
1498
+ }
1499
+ const last = Date.parse(lastRotated);
1500
+ if (Number.isNaN(last)) {
1501
+ continue;
1502
+ }
1503
+ const overdueBy = now.getTime() - last - interval;
1504
+ if (overdueBy <= 0) {
1505
+ continue;
1506
+ }
1507
+ findings.push({
1508
+ check: "rotation-overdue",
1509
+ severity: "warning",
1510
+ label: "Rotation overdue",
1511
+ subject: resolution.parameter,
1512
+ detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
1513
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
1514
+ });
1515
+ }
1516
+ if (findings.length > 0) {
1517
+ return findings;
1518
+ }
1519
+ return [
1520
+ {
1521
+ check: "rotation-overdue",
1522
+ severity: "pass",
1523
+ label: "Rotation freshness",
1524
+ subject: `no parameter is past its rotation policy for ${environment}`
1525
+ }
1526
+ ];
1527
+ }
1528
+ function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1529
+ const findings = [];
1530
+ for (const { resolution, meta } of subjects) {
1531
+ if (!(0, import_core7.isStuck)(meta, environment, now, stuckThresholdMs)) {
1532
+ continue;
1533
+ }
1534
+ const { rotatingSince } = (0, import_core7.rotationOf)(meta, environment);
1535
+ if (rotatingSince === null) {
1536
+ continue;
1537
+ }
1538
+ const openFor = now.getTime() - Date.parse(rotatingSince);
1539
+ findings.push({
1540
+ check: "rotation-stuck",
1541
+ severity: "warning",
1542
+ label: "Rotation stuck",
1543
+ subject: resolution.parameter,
1544
+ detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
1545
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
1546
+ });
1547
+ }
1548
+ if (findings.length > 0) {
1549
+ return findings;
1550
+ }
1551
+ return [
1552
+ {
1553
+ check: "rotation-stuck",
1554
+ severity: "pass",
1555
+ label: "Rotation progress",
1556
+ subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
1557
+ }
1558
+ ];
1559
+ }
1560
+ function driftKey(file) {
1561
+ return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
1562
+ }
1563
+ function scopeKey(scope) {
1564
+ switch (scope.kind) {
1565
+ case "unscoped":
1566
+ return "unscoped";
1567
+ case "environment":
1568
+ return `environment:${scope.environment}`;
1569
+ case "local":
1570
+ return "local";
1571
+ case "environment-local":
1572
+ return `environment-local:${scope.environment}`;
1573
+ default:
1574
+ return (0, import_core7.assertNever)(scope, "scope");
1575
+ }
1576
+ }
1577
+ function relevantToEnvironment(file, environment) {
1578
+ const scope = file.scope;
1579
+ if (scope.kind === "unscoped") return true;
1580
+ if (scope.kind === "environment") return scope.environment === environment;
1581
+ return false;
1582
+ }
1583
+ async function readRelevant(provider, environment) {
1584
+ const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
1585
+ const stored = await Promise.all(files.map((file) => provider.read(file)));
1586
+ const entries = /* @__PURE__ */ new Map();
1587
+ for (const [index, file] of files.entries()) {
1588
+ const value = stored[index];
1589
+ if (value !== void 0) {
1590
+ entries.set(driftKey(file), { file, stored: value });
1591
+ }
1592
+ }
1593
+ return entries;
1594
+ }
1595
+ async function providerDriftFindings(project, environment, override) {
1596
+ const providerConfig = project.config.providers[environment];
1597
+ if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
1598
+ return [
1599
+ {
1600
+ check: "provider-value-drift",
1601
+ severity: "pass",
1602
+ label: "Provider values",
1603
+ subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
1604
+ }
1605
+ ];
1606
+ }
1607
+ const source = override ?? await sourceProviderFor(project, environment);
1608
+ let local;
1609
+ let remote;
1610
+ try {
1611
+ local = await readRelevant(project.provider, environment);
1612
+ remote = await readRelevant(source, environment);
1613
+ } catch (error) {
1614
+ return [
1615
+ {
1616
+ check: "provider-value-drift",
1617
+ severity: "unknown",
1618
+ label: "Provider values",
1619
+ subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
1620
+ detail: errorDetail(error)
1621
+ }
1622
+ ];
1623
+ }
1624
+ const keys = keySourceFor(project, environment);
1625
+ const findings = [];
1626
+ const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
1627
+ for (const address of addresses) {
1628
+ const here = local.get(address);
1629
+ const there = remote.get(address);
1630
+ if (here !== void 0 && there !== void 0) {
1631
+ const opened = (0, import_core7.openValue)(here.file, here.stored, keys);
1632
+ if (opened.kind !== "plaintext") {
1633
+ findings.push({
1634
+ check: "provider-value-drift",
1635
+ severity: "unknown",
1636
+ label: "Provider value unreadable",
1637
+ subject: (0, import_core7.formatValueFile)(here.file),
1638
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
1639
+ });
1640
+ continue;
1641
+ }
1642
+ if (opened.value !== there.stored) {
1643
+ findings.push({
1644
+ check: "provider-value-drift",
1645
+ severity: "failure",
1646
+ label: "Provider value drift",
1647
+ subject: (0, import_core7.formatValueFile)(here.file),
1648
+ detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
1649
+ });
1650
+ }
1651
+ continue;
1652
+ }
1653
+ const present = here ?? there;
1654
+ if (present === void 0) {
1655
+ continue;
1656
+ }
1657
+ findings.push({
1658
+ check: "provider-value-drift",
1659
+ severity: "warning",
1660
+ label: here !== void 0 ? "Only in the local tree" : "Only in the source",
1661
+ subject: (0, import_core7.formatValueFile)(present.file),
1662
+ 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`
1663
+ });
1664
+ }
1665
+ if (findings.length > 0) {
1666
+ return findings;
1667
+ }
1668
+ return [
1669
+ {
1670
+ check: "provider-value-drift",
1671
+ severity: "pass",
1672
+ label: "Provider values",
1673
+ subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
1674
+ }
1675
+ ];
1676
+ }
1281
1677
  function renderDoctor(report) {
1282
1678
  const rows = report.findings.map((finding) => ({
1283
1679
  glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
@@ -1364,32 +1760,45 @@ function sealFor(project, file, value, parameter, environment) {
1364
1760
  }
1365
1761
  return (0, import_core8.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
1366
1762
  }
1367
- async function runSet(options) {
1368
- const project = openProject(options.cwd);
1369
- const ref = refFromKey(options.key, project.config);
1370
- const scope = targetScope(project, options, options.key);
1371
- const environment = policyEnvironment(project, options);
1372
- const secret = (0, import_core8.isSecret)(await project.provider.readMeta(ref), environment);
1763
+ async function sealAwareWrite(options) {
1764
+ const { project, provider, ref, scope, value, environment } = options;
1765
+ const secret = (0, import_core8.isSecret)(await provider.readMeta(ref), environment);
1373
1766
  const file = {
1374
1767
  namespace: ref.namespace,
1375
1768
  name: ref.name,
1376
1769
  scope,
1377
1770
  encrypted: secret
1378
1771
  };
1379
- const stored = secret ? sealFor(project, file, options.value, (0, import_core8.parameterId)(ref), environment) : options.value;
1380
- await project.provider.write(file, stored);
1381
- await project.provider.remove({ ...file, encrypted: !secret });
1382
- return { parameter: options.key, location: (0, import_core8.formatValueFile)(file), encrypted: secret };
1772
+ const stored = secret ? sealFor(project, file, value, (0, import_core8.parameterId)(ref), environment) : value;
1773
+ await provider.write(file, stored);
1774
+ await provider.remove({ ...file, encrypted: !secret });
1775
+ return { encrypted: secret, location: (0, import_core8.formatValueFile)(file) };
1776
+ }
1777
+ async function runSet(options) {
1778
+ const project = openProject(options.cwd);
1779
+ assertWritableKey(options.key);
1780
+ const ref = refFromKey(options.key, project.config);
1781
+ const scope = targetScope(project, options, options.key);
1782
+ const environment = policyEnvironment(project, options);
1783
+ const { encrypted, location } = await sealAwareWrite({
1784
+ project,
1785
+ provider: project.provider,
1786
+ ref,
1787
+ scope,
1788
+ value: options.value,
1789
+ environment
1790
+ });
1791
+ return { parameter: options.key, location, encrypted };
1383
1792
  }
1384
- function renderSet(result) {
1793
+ function renderSet(result2) {
1385
1794
  return formatRows([
1386
1795
  {
1387
1796
  glyph: CHECK,
1388
1797
  label: "Wrote",
1389
- subject: `${PENV_DIR}/${result.location}`,
1798
+ subject: `${PENV_DIR}/${result2.location}`,
1390
1799
  // The `.enc` suffix says this already, but only to a reader who knows the
1391
1800
  // grammar. Meta decided it, not the command line, so the command says so.
1392
- ...result.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1801
+ ...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1393
1802
  }
1394
1803
  ]);
1395
1804
  }
@@ -1525,13 +1934,13 @@ var UndecryptableAt = class extends import_core9.PenvError {
1525
1934
  );
1526
1935
  }
1527
1936
  };
1528
- function renderReseal(result, verb) {
1937
+ function renderReseal(result2, verb) {
1529
1938
  return formatRows([
1530
1939
  {
1531
1940
  glyph: CHECK,
1532
1941
  label: verb,
1533
- subject: `${PENV_DIR}/${result.location}`,
1534
- detail: `${PENV_DIR}/${result.removed} removed`
1942
+ subject: `${PENV_DIR}/${result2.location}`,
1943
+ detail: `${PENV_DIR}/${result2.removed} removed`
1535
1944
  }
1536
1945
  ]);
1537
1946
  }
@@ -1554,8 +1963,8 @@ var encryptCommand = (0, import_citty5.defineCommand)({
1554
1963
  args: SCOPE_ARGS,
1555
1964
  run({ args }) {
1556
1965
  return guard(async () => {
1557
- const result = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1558
- write(renderReseal(result, "Encrypted"));
1966
+ const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1967
+ write(renderReseal(result2, "Encrypted"));
1559
1968
  });
1560
1969
  }
1561
1970
  });
@@ -1564,23 +1973,128 @@ var decryptCommand = (0, import_citty5.defineCommand)({
1564
1973
  args: SCOPE_ARGS,
1565
1974
  run({ args }) {
1566
1975
  return guard(async () => {
1567
- const result = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1568
- write(renderReseal(result, "Decrypted"));
1976
+ const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1977
+ write(renderReseal(result2, "Decrypted"));
1569
1978
  });
1570
1979
  }
1571
1980
  });
1572
1981
 
1573
- // src/commands/generate.ts
1574
- var import_node_fs = require("fs");
1575
- var import_node_path3 = require("path");
1982
+ // src/commands/fill.ts
1983
+ var import_promises = require("readline/promises");
1576
1984
  var import_core10 = require("@penvhq/core");
1577
1985
  var import_citty6 = require("citty");
1986
+ var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1987
+ async function runFill(options) {
1988
+ const validation = await runValidate({
1989
+ cwd: options.cwd,
1990
+ ...options.environment === void 0 ? {} : { environment: options.environment }
1991
+ });
1992
+ const environment = validation.environment;
1993
+ const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
1994
+ if (blockers.length > 0) {
1995
+ const detail = blockers.map(
1996
+ (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
1997
+ ).join("\n");
1998
+ throw new import_core10.PenvError(
1999
+ "FILL_BLOCKED",
2000
+ `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
2001
+ ${detail}`,
2002
+ `Fix these \u2014 \`penv validate\` reports them \u2014 then run \`penv fill\`. If you have not written a schema yet, declare the required parameters in ${PENV_DIR}/env.ts.`
2003
+ );
2004
+ }
2005
+ const written = [];
2006
+ const skipped2 = [];
2007
+ const unreachable = [];
2008
+ for (const drift of validation.drift.declared) {
2009
+ if (drift.ref === void 0) {
2010
+ unreachable.push({ subject: drift.subject, remedy: drift.remedy });
2011
+ continue;
2012
+ }
2013
+ const ref = drift.ref;
2014
+ const key = [...ref.namespace, ref.name].join("/");
2015
+ const value = await options.ask({ parameter: key, environment, secret: false });
2016
+ if (value === void 0 || value === "") {
2017
+ skipped2.push(drift.subject);
2018
+ continue;
2019
+ }
2020
+ const result2 = await runSet({ cwd: options.cwd, key, value, environment });
2021
+ written.push({
2022
+ parameter: drift.subject,
2023
+ location: result2.location,
2024
+ encrypted: result2.encrypted
2025
+ });
2026
+ }
2027
+ return { environment, written, skipped: skipped2, unreachable };
2028
+ }
2029
+ function summaryLine(result2) {
2030
+ const filled = result2.written.length;
2031
+ if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
2032
+ return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2033
+ }
2034
+ const parts = [`${filled} written`];
2035
+ if (result2.skipped.length > 0) {
2036
+ parts.push(`${result2.skipped.length} skipped`);
2037
+ }
2038
+ if (result2.unreachable.length > 0) {
2039
+ parts.push(`${result2.unreachable.length} unreachable`);
2040
+ }
2041
+ return `${parts.join(", ")} for environment ${result2.environment}`;
2042
+ }
2043
+ function renderFill(result2) {
2044
+ const rows = result2.written.map((entry) => ({
2045
+ glyph: CHECK,
2046
+ label: "Wrote",
2047
+ subject: `${PENV_DIR}/${entry.location}`,
2048
+ // Meta decided the seal, not the answer, so the line says so — as `set` does.
2049
+ ...entry.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
2050
+ }));
2051
+ for (const entry of result2.unreachable) {
2052
+ rows.push({ glyph: WARN, label: "Unreachable", subject: entry.subject, detail: entry.remedy });
2053
+ }
2054
+ const lines = formatRows(rows);
2055
+ lines.push(summaryLine(result2));
2056
+ return lines;
2057
+ }
2058
+ var fillCommand = (0, import_citty6.defineCommand)({
2059
+ meta: {
2060
+ name: "fill",
2061
+ description: "Prompt for each declared parameter the tree has no value for"
2062
+ },
2063
+ args: {
2064
+ env: { type: "string", description: "The environment to fill" }
2065
+ },
2066
+ run({ args }) {
2067
+ return guard(async () => {
2068
+ const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
2069
+ const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2070
+ try {
2071
+ write(
2072
+ renderFill(
2073
+ await runFill({
2074
+ cwd: process.cwd(),
2075
+ ...args.env === void 0 ? {} : { environment: args.env },
2076
+ ask
2077
+ })
2078
+ )
2079
+ );
2080
+ } finally {
2081
+ rl.close();
2082
+ }
2083
+ });
2084
+ }
2085
+ });
2086
+
2087
+ // src/commands/generate.ts
2088
+ var import_node_fs = require("fs");
2089
+ var import_node_path4 = require("path");
2090
+ var import_core11 = require("@penvhq/core");
2091
+ var import_citty7 = require("citty");
1578
2092
  var DEFAULT_OUTPUT = ".env";
1579
2093
  function entriesFor(project, environment, allowDecrypt) {
1580
2094
  const keys = keySourceFor(project, environment);
1581
2095
  const tree = localTree(project);
1582
2096
  const resolutions = resolveAllSync(tree, environment, keys);
1583
- const collision = (0, import_core10.checkNameCollisions)(
2097
+ const collision = (0, import_core11.checkNameCollisions)(
1584
2098
  refsFrom(resolutions.map((resolution) => resolution.ref)),
1585
2099
  project.config
1586
2100
  )[0];
@@ -1593,21 +2107,21 @@ function entriesFor(project, environment, allowDecrypt) {
1593
2107
  const winner = resolution.winner;
1594
2108
  if (winner?.file.encrypted === true) {
1595
2109
  if (!allowDecrypt) {
1596
- throw new import_core10.PenvError(
2110
+ throw new import_core11.PenvError(
1597
2111
  "ENCRYPTED_VALUE_REFUSED",
1598
2112
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
1599
2113
  `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.`
1600
2114
  );
1601
2115
  }
1602
- (0, import_core10.requireValue)(resolution, environment);
2116
+ (0, import_core11.requireValue)(resolution, environment);
1603
2117
  decrypted += 1;
1604
2118
  }
1605
2119
  if (resolution.value === void 0) {
1606
2120
  continue;
1607
2121
  }
1608
- const description = (0, import_core10.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
2122
+ const description = (0, import_core11.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
1609
2123
  entries.push({
1610
- key: (0, import_core10.variableName)(resolution.ref, project.config),
2124
+ key: (0, import_core11.variableName)(resolution.ref, project.config),
1611
2125
  value: resolution.value,
1612
2126
  ...typeof description === "string" ? { description } : {}
1613
2127
  });
@@ -1618,40 +2132,40 @@ function entriesFor(project, environment, allowDecrypt) {
1618
2132
  function generateDotenv(options) {
1619
2133
  const project = openProject(options.cwd);
1620
2134
  const environment = targetEnvironment(project, options.environment);
1621
- return (0, import_core10.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
2135
+ return (0, import_core11.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
1622
2136
  }
1623
2137
  function runGenerate(options) {
1624
2138
  const project = openProject(options.cwd);
1625
2139
  const environment = targetEnvironment(project, options.environment);
1626
2140
  const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
1627
- const file = options.out === void 0 ? (0, import_node_path3.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path3.isAbsolute)(options.out) ? options.out : (0, import_node_path3.resolve)(options.cwd, options.out);
1628
- (0, import_node_fs.writeFileSync)(file, (0, import_core10.serializeDotenv)(entries), "utf8");
2141
+ 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);
2142
+ (0, import_node_fs.writeFileSync)(file, (0, import_core11.serializeDotenv)(entries), "utf8");
1629
2143
  return { file, environment, entries: entries.length, decrypted };
1630
2144
  }
1631
2145
  function displayPath(cwd, file) {
1632
- const rel = (0, import_node_path3.relative)(cwd, file);
2146
+ const rel = (0, import_node_path4.relative)(cwd, file);
1633
2147
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
1634
2148
  }
1635
- function renderGenerate(result, cwd) {
2149
+ function renderGenerate(result2, cwd) {
1636
2150
  const rows = [
1637
2151
  {
1638
2152
  glyph: CHECK,
1639
2153
  label: "Generated",
1640
- subject: displayPath(cwd, result.file),
1641
- detail: `${result.entries} variables for environment ${result.environment}`
2154
+ subject: displayPath(cwd, result2.file),
2155
+ detail: `${result2.entries} variables for environment ${result2.environment}`
1642
2156
  }
1643
2157
  ];
1644
- if (result.decrypted > 0) {
2158
+ if (result2.decrypted > 0) {
1645
2159
  rows.push({
1646
2160
  glyph: WARN,
1647
2161
  label: "Decrypted",
1648
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
2162
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
1649
2163
  detail: "written as plaintext into the artifact"
1650
2164
  });
1651
2165
  }
1652
2166
  return formatRows(rows);
1653
2167
  }
1654
- var generateCommand = (0, import_citty6.defineCommand)({
2168
+ var generateCommand = (0, import_citty7.defineCommand)({
1655
2169
  meta: { name: "generate", description: "Write a standard .env artifact for deploy targets" },
1656
2170
  args: {
1657
2171
  env: { type: "string", description: "The environment to generate for" },
@@ -1664,29 +2178,29 @@ var generateCommand = (0, import_citty6.defineCommand)({
1664
2178
  run({ args }) {
1665
2179
  return guard(async () => {
1666
2180
  const cwd = process.cwd();
1667
- const result = runGenerate({
2181
+ const result2 = runGenerate({
1668
2182
  cwd,
1669
2183
  ...args.env === void 0 ? {} : { environment: args.env },
1670
2184
  ...args.out === void 0 ? {} : { out: args.out },
1671
2185
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1672
2186
  });
1673
- write(renderGenerate(result, cwd));
2187
+ write(renderGenerate(result2, cwd));
1674
2188
  });
1675
2189
  }
1676
2190
  });
1677
2191
 
1678
2192
  // src/commands/get.ts
1679
- var import_core11 = require("@penvhq/core");
1680
- var import_citty7 = require("citty");
2193
+ var import_core12 = require("@penvhq/core");
2194
+ var import_citty8 = require("citty");
1681
2195
  async function runGet(options) {
1682
2196
  const project = openProject(options.cwd);
1683
2197
  const environment = targetEnvironment(project, options.environment);
1684
2198
  const ref = refFromKey(options.key);
1685
2199
  const keys = keySourceFor(project, environment);
1686
- const resolution = await (0, import_core11.resolveParameter)(ref, environment, project.provider, keys);
1687
- const value = (0, import_core11.requireValue)(resolution, environment);
2200
+ const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
2201
+ const value = (0, import_core12.requireValue)(resolution, environment);
1688
2202
  if (value === void 0) {
1689
- throw new import_core11.PenvError(
2203
+ throw new import_core12.PenvError(
1690
2204
  "PARAMETER_ABSENT",
1691
2205
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
1692
2206
  `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.`
@@ -1708,7 +2222,7 @@ async function runExplain(options) {
1708
2222
  const environment = targetEnvironment(project, options.environment);
1709
2223
  const ref = refFromKey(options.key);
1710
2224
  const keys = keySourceFor(project, environment);
1711
- const resolution = await (0, import_core11.resolveParameter)(ref, environment, project.provider, keys);
2225
+ const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
1712
2226
  const winner = resolution.winner;
1713
2227
  return {
1714
2228
  parameter: resolution.parameter,
@@ -1736,7 +2250,7 @@ function renderExplain(explanation) {
1736
2250
  ...columns(rows).map((line) => ` ${line}`)
1737
2251
  ];
1738
2252
  }
1739
- var getCommand = (0, import_citty7.defineCommand)({
2253
+ var getCommand = (0, import_citty8.defineCommand)({
1740
2254
  meta: { name: "get", description: "Read a parameter" },
1741
2255
  args: {
1742
2256
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -1761,14 +2275,14 @@ var getCommand = (0, import_citty7.defineCommand)({
1761
2275
 
1762
2276
  // src/commands/import.ts
1763
2277
  var import_node_fs4 = require("fs");
1764
- var import_node_path6 = require("path");
1765
- var import_core14 = require("@penvhq/core");
1766
- var import_citty9 = require("citty");
2278
+ var import_node_path7 = require("path");
2279
+ var import_core15 = require("@penvhq/core");
2280
+ var import_citty10 = require("citty");
1767
2281
 
1768
2282
  // src/detect.ts
1769
2283
  var import_node_fs2 = require("fs");
1770
- var import_node_path4 = require("path");
1771
- var import_core12 = require("@penvhq/core");
2284
+ var import_node_path5 = require("path");
2285
+ var import_core13 = require("@penvhq/core");
1772
2286
  var SIGNATURES = [
1773
2287
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
1774
2288
  {
@@ -1793,11 +2307,11 @@ function exportsSchema(file) {
1793
2307
  );
1794
2308
  }
1795
2309
  function occupied(cwd, relative3) {
1796
- const file = (0, import_node_path4.join)(cwd, ...relative3.split("/"));
2310
+ const file = (0, import_node_path5.join)(cwd, ...relative3.split("/"));
1797
2311
  return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
1798
2312
  }
1799
2313
  function schemaFileFor(cwd) {
1800
- const dir = (0, import_node_fs2.existsSync)((0, import_node_path4.join)(cwd, "src")) ? "src/" : "";
2314
+ const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
1801
2315
  const preferred = `${dir}env.ts`;
1802
2316
  if (!occupied(cwd, preferred)) {
1803
2317
  return { file: preferred };
@@ -1806,7 +2320,7 @@ function schemaFileFor(cwd) {
1806
2320
  if (!occupied(cwd, beside)) {
1807
2321
  return { file: beside, displaced: preferred };
1808
2322
  }
1809
- return { file: import_core12.DEFAULT_SCHEMA_FILE, displaced: preferred };
2323
+ return { file: import_core13.DEFAULT_SCHEMA_FILE, displaced: preferred };
1810
2324
  }
1811
2325
  function dependenciesOf(cwd) {
1812
2326
  const manifest = manifestOf(cwd);
@@ -1825,7 +2339,7 @@ function dependenciesOf(cwd) {
1825
2339
  return names;
1826
2340
  }
1827
2341
  function manifestOf(cwd) {
1828
- const file = (0, import_node_path4.join)(cwd, "package.json");
2342
+ const file = (0, import_node_path5.join)(cwd, "package.json");
1829
2343
  if (!(0, import_node_fs2.existsSync)(file)) {
1830
2344
  return void 0;
1831
2345
  }
@@ -1868,10 +2382,10 @@ function hasImportsBlock(cwd) {
1868
2382
 
1869
2383
  // src/commands/init.ts
1870
2384
  var import_node_fs3 = require("fs");
1871
- var import_node_path5 = require("path");
1872
- var import_promises = require("readline/promises");
1873
- var import_core13 = require("@penvhq/core");
1874
- var import_citty8 = require("citty");
2385
+ var import_node_path6 = require("path");
2386
+ var import_promises2 = require("readline/promises");
2387
+ var import_core14 = require("@penvhq/core");
2388
+ var import_citty9 = require("citty");
1875
2389
  var CONFIG_FILE = "penv.config.ts";
1876
2390
  var TSCONFIG_FILE = "tsconfig.json";
1877
2391
  var GITIGNORE_FILE = ".gitignore";
@@ -1881,11 +2395,11 @@ var IMPORTS_PREFIX = "#";
1881
2395
  var PACKAGE_FILE = "package.json";
1882
2396
  var DEFAULT_DECISIONS = {
1883
2397
  environments: [],
1884
- schemaFile: import_core13.DEFAULT_SCHEMA_FILE,
2398
+ schemaFile: import_core14.DEFAULT_SCHEMA_FILE,
1885
2399
  publicPrefixes: [],
1886
2400
  alias: DEFAULT_ALIAS
1887
2401
  };
1888
- var NOT_ENVIRONMENTS = [...import_core13.RESERVED_TOKENS, "example", "sample", "template"];
2402
+ var NOT_ENVIRONMENTS = [...import_core14.RESERVED_TOKENS, "example", "sample", "template"];
1889
2403
  function suggestEnvironments(root) {
1890
2404
  let entries;
1891
2405
  try {
@@ -1901,7 +2415,7 @@ function suggestEnvironments(root) {
1901
2415
  const segments = entry.slice(".env.".length).split(".");
1902
2416
  const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
1903
2417
  const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
1904
- if (name === void 0 || !(0, import_core13.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
2418
+ if (name === void 0 || !(0, import_core14.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
1905
2419
  continue;
1906
2420
  }
1907
2421
  found.add(name);
@@ -1909,10 +2423,10 @@ function suggestEnvironments(root) {
1909
2423
  return [...found].sort();
1910
2424
  }
1911
2425
  function emptyFlag(flag) {
1912
- return new import_core13.PenvError(
2426
+ return new import_core14.PenvError(
1913
2427
  "INIT_FLAG_EMPTY",
1914
2428
  `\`--${flag}\` was given without a value`,
1915
- flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core13.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."
2429
+ 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."
1916
2430
  );
1917
2431
  }
1918
2432
  function splitEnvironments(value) {
@@ -1933,11 +2447,11 @@ function configOf(decisions) {
1933
2447
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
1934
2448
  }
1935
2449
  function declaredIn(root) {
1936
- const file = (0, import_node_path5.join)(root, CONFIG_FILE);
2450
+ const file = (0, import_node_path6.join)(root, CONFIG_FILE);
1937
2451
  if (!(0, import_node_fs3.existsSync)(file)) {
1938
2452
  return void 0;
1939
2453
  }
1940
- return (0, import_core13.loadConfigFrom)(file);
2454
+ return (0, import_core14.loadConfigFrom)(file);
1941
2455
  }
1942
2456
  function planInit(root, flags = {}) {
1943
2457
  const declared = declaredIn(root);
@@ -1947,7 +2461,7 @@ function planInit(root, flags = {}) {
1947
2461
  notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
1948
2462
  } else if (detected === void 0) {
1949
2463
  notes.push(
1950
- `No framework detected in package.json \u2014 the schema goes to ${import_core13.DEFAULT_SCHEMA_FILE}.`
2464
+ `No framework detected in package.json \u2014 the schema goes to ${import_core14.DEFAULT_SCHEMA_FILE}.`
1951
2465
  );
1952
2466
  } else {
1953
2467
  notes.push(`Detected ${detected.name}.`);
@@ -1957,12 +2471,12 @@ function planInit(root, flags = {}) {
1957
2471
  );
1958
2472
  }
1959
2473
  }
1960
- const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core13.schemaFileOf)(declared) : detected?.schemaFile ?? import_core13.DEFAULT_SCHEMA_FILE : flags.schema.trim();
2474
+ const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core14.schemaFileOf)(declared) : detected?.schemaFile ?? import_core14.DEFAULT_SCHEMA_FILE : flags.schema.trim();
1961
2475
  if (flags.schema !== void 0) {
1962
2476
  if (schemaFile.length === 0) {
1963
2477
  throw emptyFlag("schema");
1964
2478
  }
1965
- const error = (0, import_core13.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
2479
+ const error = (0, import_core14.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
1966
2480
  if (error !== void 0) {
1967
2481
  throw error;
1968
2482
  }
@@ -1972,7 +2486,7 @@ function planInit(root, flags = {}) {
1972
2486
  throw emptyFlag("alias");
1973
2487
  }
1974
2488
  if (!ALIAS_NAME.test(alias)) {
1975
- throw new import_core13.PenvError(
2489
+ throw new import_core14.PenvError(
1976
2490
  "INIT_ALIAS_INVALID",
1977
2491
  `\`${alias}\` is not an alias penv can write`,
1978
2492
  `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.`
@@ -2017,7 +2531,7 @@ function renderPlan(plan2) {
2017
2531
  rows.push([
2018
2532
  " schemaFile",
2019
2533
  plan2.decisions.schemaFile,
2020
- plan2.decisions.schemaFile === import_core13.DEFAULT_SCHEMA_FILE ? "" : `(default: ${import_core13.DEFAULT_SCHEMA_FILE})`
2534
+ plan2.decisions.schemaFile === import_core14.DEFAULT_SCHEMA_FILE ? "" : `(default: ${import_core14.DEFAULT_SCHEMA_FILE})`
2021
2535
  ]);
2022
2536
  for (const prefix of plan2.decisions.publicPrefixes) {
2023
2537
  rows.push([" publicPrefix", prefix, ""]);
@@ -2091,7 +2605,7 @@ ${providers} },
2091
2605
  }
2092
2606
  function renderConfigModule(decisions) {
2093
2607
  let body = renderEnvironments(decisions);
2094
- if (decisions.schemaFile !== import_core13.DEFAULT_SCHEMA_FILE) {
2608
+ if (decisions.schemaFile !== import_core14.DEFAULT_SCHEMA_FILE) {
2095
2609
  body += `
2096
2610
  // The module that exports the schema. It is yours \u2014 penv scaffolds it once
2097
2611
  // and never regenerates it \u2014 so this says where you keep it.
@@ -2114,7 +2628,7 @@ ${body}});
2114
2628
  `;
2115
2629
  }
2116
2630
  function renderGitignore(decisions) {
2117
- const inside = (0, import_core13.schemaInsideTree)(configOf(decisions));
2631
+ const inside = (0, import_core14.schemaInsideTree)(configOf(decisions));
2118
2632
  const listed = inside === void 0 ? "" : `${inside}, `;
2119
2633
  return `# Written by penv. Value files hold configuration values and are never
2120
2634
  # committed; only the structure, ${listed}meta, and config are.
@@ -2259,13 +2773,13 @@ ${objectIndent}${source.slice(close)}`;
2259
2773
  ${entryIndent}${member},${source.slice(open + 1)}`;
2260
2774
  }
2261
2775
  function shapeError(what, target, alias) {
2262
- return new import_core13.PenvError(
2776
+ return new import_core14.PenvError(
2263
2777
  "TSCONFIG_SHAPE",
2264
2778
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
2265
2779
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
2266
2780
  );
2267
2781
  }
2268
- function insertEnvAlias(source, target = import_core13.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
2782
+ function insertEnvAlias(source, target = import_core14.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
2269
2783
  const alias = `"${name}": ["${target}"]`;
2270
2784
  const root = skipTrivia(source, 0);
2271
2785
  if (source.charAt(root) !== "{") {
@@ -2329,7 +2843,7 @@ function renderTsconfig(target, alias) {
2329
2843
  `;
2330
2844
  }
2331
2845
  function ensurePenvDir(root) {
2332
- const dir = (0, import_node_path5.resolve)(root, PENV_DIR2);
2846
+ const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
2333
2847
  if ((0, import_node_fs3.existsSync)(dir)) {
2334
2848
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
2335
2849
  }
@@ -2337,7 +2851,7 @@ function ensurePenvDir(root) {
2337
2851
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
2338
2852
  }
2339
2853
  function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2340
- const file = (0, import_node_path5.join)(root, ...decisions.schemaFile.split("/"));
2854
+ const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
2341
2855
  if ((0, import_node_fs3.existsSync)(file)) {
2342
2856
  return {
2343
2857
  target: "schema",
@@ -2346,7 +2860,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2346
2860
  note: "(yours \u2014 penv never regenerates it)"
2347
2861
  };
2348
2862
  }
2349
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.dirname)(file), { recursive: true });
2863
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
2350
2864
  (0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
2351
2865
  return {
2352
2866
  target: "schema",
@@ -2356,7 +2870,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2356
2870
  };
2357
2871
  }
2358
2872
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
2359
- const file = (0, import_node_path5.join)(root, CONFIG_FILE);
2873
+ const file = (0, import_node_path6.join)(root, CONFIG_FILE);
2360
2874
  if ((0, import_node_fs3.existsSync)(file)) {
2361
2875
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
2362
2876
  }
@@ -2366,7 +2880,7 @@ function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
2366
2880
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
2367
2881
  const alias = decisions.alias;
2368
2882
  const imports = alias.startsWith(IMPORTS_PREFIX);
2369
- const file = (0, import_node_path5.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
2883
+ const file = (0, import_node_path6.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
2370
2884
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
2371
2885
  if (!(0, import_node_fs3.existsSync)(file)) {
2372
2886
  if (imports) {
@@ -2409,14 +2923,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
2409
2923
  };
2410
2924
  }
2411
2925
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
2412
- const file = (0, import_node_path5.join)(root, PENV_DIR2, GITIGNORE_FILE);
2926
+ const file = (0, import_node_path6.join)(root, PENV_DIR2, GITIGNORE_FILE);
2413
2927
  const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
2414
2928
  const wanted = renderGitignore(decisions);
2415
2929
  const existing = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : void 0;
2416
2930
  if (existing === wanted) {
2417
2931
  return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
2418
2932
  }
2419
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(root, PENV_DIR2), { recursive: true });
2933
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
2420
2934
  (0, import_node_fs3.writeFileSync)(file, wanted, "utf8");
2421
2935
  return {
2422
2936
  target: "gitignore",
@@ -2434,26 +2948,26 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2434
2948
  ];
2435
2949
  }
2436
2950
  function runInit(options) {
2437
- const root = (0, import_node_path5.resolve)(options.cwd);
2951
+ const root = (0, import_node_path6.resolve)(options.cwd);
2438
2952
  const decisions = options.decisions ?? planInit(root).decisions;
2439
2953
  return { root, decisions, steps: scaffold(root, [], false, decisions) };
2440
2954
  }
2441
- function renderInit(result) {
2442
- const steps = result.steps.map((step) => {
2955
+ function renderInit(result2) {
2956
+ const steps = result2.steps.map((step) => {
2443
2957
  const glyph = step.action === "conflicted" ? WARN : CHECK;
2444
2958
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
2445
2959
  });
2446
2960
  return [
2447
2961
  ...formatSteps(steps),
2448
2962
  "",
2449
- `Done. Declare your parameters in ${result.decisions.schemaFile}, then \`penv set <key>\`.`,
2450
- ...result.decisions.environments.length === 0 ? [
2963
+ `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
2964
+ ...result2.decisions.environments.length === 0 ? [
2451
2965
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2452
2966
  ] : []
2453
2967
  ];
2454
2968
  }
2455
2969
  async function askOnTty(plan2) {
2456
- const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
2970
+ const rl = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
2457
2971
  try {
2458
2972
  return await promptForDecisions(plan2, {
2459
2973
  ask: (question) => rl.question(question),
@@ -2464,7 +2978,7 @@ async function askOnTty(plan2) {
2464
2978
  rl.close();
2465
2979
  }
2466
2980
  }
2467
- var initCommand = (0, import_citty8.defineCommand)({
2981
+ var initCommand = (0, import_citty9.defineCommand)({
2468
2982
  meta: { name: "init", description: "Initialize a project (.penv/, env.ts, config, @env alias)" },
2469
2983
  args: {
2470
2984
  yes: {
@@ -2473,7 +2987,7 @@ var initCommand = (0, import_citty8.defineCommand)({
2473
2987
  },
2474
2988
  schema: {
2475
2989
  type: "string",
2476
- description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core13.DEFAULT_SCHEMA_FILE})`
2990
+ description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core14.DEFAULT_SCHEMA_FILE})`
2477
2991
  },
2478
2992
  alias: {
2479
2993
  type: "string",
@@ -2486,7 +3000,7 @@ var initCommand = (0, import_citty8.defineCommand)({
2486
3000
  },
2487
3001
  run({ args }) {
2488
3002
  return guard(async () => {
2489
- const root = (0, import_node_path5.resolve)(process.cwd());
3003
+ const root = (0, import_node_path6.resolve)(process.cwd());
2490
3004
  const environments = environmentsFromFlag(args.env);
2491
3005
  const plan2 = planInit(root, {
2492
3006
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -2527,7 +3041,7 @@ function inferType(value) {
2527
3041
  }
2528
3042
  function draftFields(entries) {
2529
3043
  return entries.map((entry) => {
2530
- const key = (0, import_core14.accessPath)((0, import_core14.refFromVariable)(entry.key)).join(".");
3044
+ const key = (0, import_core15.accessPath)((0, import_core15.refFromVariable)(entry.key)).join(".");
2531
3045
  return {
2532
3046
  key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
2533
3047
  type: inferType(entry.value)
@@ -2538,33 +3052,33 @@ function assertImportable(ref, variable) {
2538
3052
  if (!ref.name.includes(".")) {
2539
3053
  return;
2540
3054
  }
2541
- throw new import_core14.PenvError(
3055
+ throw new import_core15.PenvError(
2542
3056
  "IMPORT_UNPARSEABLE_NAME",
2543
3057
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
2544
3058
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
2545
3059
  );
2546
3060
  }
2547
3061
  function assertNotReserved(ref, variable, where, config) {
2548
- if ((0, import_core14.isReservedToken)(ref.name, config)) {
2549
- throw new import_core14.ReservedTokenError("parameter", variable, where);
3062
+ if ((0, import_core15.isReservedToken)(ref.name, config)) {
3063
+ throw new import_core15.ReservedTokenError("parameter", variable, where);
2550
3064
  }
2551
3065
  }
2552
3066
  function assertRoundTrips(ref, variable, config) {
2553
- if ((0, import_core14.roundTripsCleanly)(variable)) {
3067
+ if ((0, import_core15.roundTripsCleanly)(variable)) {
2554
3068
  return;
2555
3069
  }
2556
- if ((0, import_core14.variableName)(ref, config) === variable) {
3070
+ if ((0, import_core15.variableName)(ref, config) === variable) {
2557
3071
  return;
2558
3072
  }
2559
- const generated = (0, import_core14.variableName)(ref, config);
2560
- throw new import_core14.PenvError(
3073
+ const generated = (0, import_core15.variableName)(ref, config);
3074
+ throw new import_core15.PenvError(
2561
3075
  "IMPORT_LOSSY_NAME",
2562
3076
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
2563
3077
  `\`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.`
2564
3078
  );
2565
3079
  }
2566
3080
  function collisionsIn(refs, config) {
2567
- const errors = (0, import_core14.checkNameCollisions)(refs, config);
3081
+ const errors = (0, import_core15.checkNameCollisions)(refs, config);
2568
3082
  const first = errors[0];
2569
3083
  if (first !== void 0) {
2570
3084
  throw first;
@@ -2574,10 +3088,10 @@ function assertDeclared(segment, config) {
2574
3088
  if (config.environments.includes(segment)) {
2575
3089
  return segment;
2576
3090
  }
2577
- throw new import_core14.UnknownEnvironmentError(segment, config.environments);
3091
+ throw new import_core15.UnknownEnvironmentError(segment, config.environments);
2578
3092
  }
2579
3093
  function scopeFromFilename(file, config) {
2580
- const name = (0, import_node_path6.basename)(file);
3094
+ const name = (0, import_node_path7.basename)(file);
2581
3095
  const segments = name.split(".");
2582
3096
  const start = segments.indexOf(DOTENV_SEGMENT);
2583
3097
  if (start === -1) {
@@ -2599,13 +3113,13 @@ function scopeFromFilename(file, config) {
2599
3113
  return { kind: "environment-local", environment: assertDeclared(first, config) };
2600
3114
  }
2601
3115
  if (first === LOCAL && rest.length === 2) {
2602
- throw new import_core14.FilenameGrammarError(
3116
+ throw new import_core15.FilenameGrammarError(
2603
3117
  name,
2604
3118
  "`local` precedes the environment segment",
2605
3119
  `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.`
2606
3120
  );
2607
3121
  }
2608
- throw new import_core14.FilenameGrammarError(
3122
+ throw new import_core15.FilenameGrammarError(
2609
3123
  name,
2610
3124
  `\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
2611
3125
  "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."
@@ -2620,7 +3134,7 @@ function environmentOf(scope) {
2620
3134
  case "local":
2621
3135
  return void 0;
2622
3136
  default:
2623
- return (0, import_core14.assertNever)(scope, "scope");
3137
+ return (0, import_core15.assertNever)(scope, "scope");
2624
3138
  }
2625
3139
  }
2626
3140
  function scopeWithEnvironment(scope, environment) {
@@ -2633,7 +3147,7 @@ function scopeWithEnvironment(scope, environment) {
2633
3147
  case "environment-local":
2634
3148
  return scope;
2635
3149
  default:
2636
- return (0, import_core14.assertNever)(scope, "scope");
3150
+ return (0, import_core15.assertNever)(scope, "scope");
2637
3151
  }
2638
3152
  }
2639
3153
  function explicitEnvironment(options, source, config) {
@@ -2641,7 +3155,7 @@ function explicitEnvironment(options, source, config) {
2641
3155
  if (value.length > 0) {
2642
3156
  return value;
2643
3157
  }
2644
- throw new import_core14.PenvError(
3158
+ throw new import_core15.PenvError(
2645
3159
  "IMPORT_ENV_FLAG_EMPTY",
2646
3160
  `\`--env\` for the import of ${source} names no environment`,
2647
3161
  `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.`
@@ -2651,7 +3165,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
2651
3165
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
2652
3166
  return;
2653
3167
  }
2654
- throw new import_core14.PenvError(
3168
+ throw new import_core15.PenvError(
2655
3169
  "IMPORT_ENV_CONFLICT",
2656
3170
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
2657
3171
  `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.`
@@ -2661,7 +3175,7 @@ function environmentNamed(file, explicit) {
2661
3175
  if (explicit !== void 0 && explicit.trim().length > 0) {
2662
3176
  return explicit.trim();
2663
3177
  }
2664
- const segments = (0, import_node_path6.basename)(file).split(".");
3178
+ const segments = (0, import_node_path7.basename)(file).split(".");
2665
3179
  const start = segments.indexOf(DOTENV_SEGMENT);
2666
3180
  if (start === -1) {
2667
3181
  return void 0;
@@ -2672,15 +3186,15 @@ function environmentNamed(file, explicit) {
2672
3186
  function decisionsOf(config, cwd) {
2673
3187
  return {
2674
3188
  environments: config.environments,
2675
- schemaFile: (0, import_core14.schemaFileOf)(config),
3189
+ schemaFile: (0, import_core15.schemaFileOf)(config),
2676
3190
  publicPrefixes: config.publicPrefixes ?? [],
2677
3191
  alias: detectAlias(cwd)
2678
3192
  };
2679
3193
  }
2680
3194
  function configInEffect(cwd, environment) {
2681
- const existing = (0, import_core14.findConfigFile)(cwd);
3195
+ const existing = (0, import_core15.findConfigFile)(cwd);
2682
3196
  if (existing !== void 0) {
2683
- const config = (0, import_core14.loadConfigFrom)(existing);
3197
+ const config = (0, import_core15.loadConfigFrom)(existing);
2684
3198
  return { config, decisions: decisionsOf(config, cwd) };
2685
3199
  }
2686
3200
  const planned = planInit(cwd).decisions;
@@ -2692,16 +3206,16 @@ function configInEffect(cwd, environment) {
2692
3206
  return { config: openProject(cwd).config, decisions };
2693
3207
  }
2694
3208
  function importDotenv(options) {
2695
- const cwd = (0, import_node_path6.resolve)(options.cwd);
2696
- const file = (0, import_node_path6.isAbsolute)(options.file) ? options.file : (0, import_node_path6.resolve)(cwd, options.file);
3209
+ const cwd = (0, import_node_path7.resolve)(options.cwd);
3210
+ const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
2697
3211
  if (!(0, import_node_fs4.existsSync)(file)) {
2698
- throw new import_core14.PenvError(
3212
+ throw new import_core15.PenvError(
2699
3213
  "IMPORT_FILE_MISSING",
2700
3214
  `There is no file at ${file} to import`,
2701
3215
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
2702
3216
  );
2703
3217
  }
2704
- const parsed = (0, import_core14.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
3218
+ const parsed = (0, import_core15.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
2705
3219
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
2706
3220
  const source = displayPath2(cwd, file);
2707
3221
  const named = scopeFromFilename(file, config);
@@ -2709,10 +3223,10 @@ function importDotenv(options) {
2709
3223
  const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
2710
3224
  assertEnvironmentAgrees(derived, explicit, source);
2711
3225
  const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
2712
- const environment = (0, import_core14.lookupEnvironment)(config, explicit ?? derived);
3226
+ const environment = (0, import_core15.lookupEnvironment)(config, explicit ?? derived);
2713
3227
  const refs = [];
2714
3228
  for (const entry of parsed.entries) {
2715
- const ref = (0, import_core14.refFromVariable)(entry.key);
3229
+ const ref = (0, import_core15.refFromVariable)(entry.key);
2716
3230
  assertImportable(ref, entry.key);
2717
3231
  assertNotReserved(ref, entry.key, source, config);
2718
3232
  assertRoundTrips(ref, entry.key, config);
@@ -2752,7 +3266,7 @@ function importDotenv(options) {
2752
3266
  };
2753
3267
  }
2754
3268
  function displayPath2(root, file) {
2755
- const rel = (0, import_node_path6.relative)(root, file);
3269
+ const rel = (0, import_node_path7.relative)(root, file);
2756
3270
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
2757
3271
  }
2758
3272
  function keptSchemaStep(step, variables) {
@@ -2776,19 +3290,19 @@ function skippedValidationStep(environments) {
2776
3290
  note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
2777
3291
  };
2778
3292
  }
2779
- function renderImport(result, validation) {
2780
- const steps = [{ glyph: CHECK, text: `Found ${result.variables} variables` }];
2781
- if (result.orphanComments > 0) {
2782
- const plural = result.orphanComments === 1 ? "comment" : "comments";
3293
+ function renderImport(result2, validation) {
3294
+ const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
3295
+ if (result2.orphanComments > 0) {
3296
+ const plural = result2.orphanComments === 1 ? "comment" : "comments";
2783
3297
  steps.push({
2784
3298
  glyph: WARN,
2785
- text: `Dropped ${result.orphanComments} orphan ${plural}`,
3299
+ text: `Dropped ${result2.orphanComments} orphan ${plural}`,
2786
3300
  note: "attached to no variable, so nothing to describe"
2787
3301
  });
2788
3302
  }
2789
- for (const step of result.steps) {
3303
+ for (const step of result2.steps) {
2790
3304
  if (step.target === "schema" && step.action === "kept") {
2791
- steps.push(keptSchemaStep(step, result.variables));
3305
+ steps.push(keptSchemaStep(step, result2.variables));
2792
3306
  continue;
2793
3307
  }
2794
3308
  const glyph = step.action === "conflicted" ? WARN : CHECK;
@@ -2796,10 +3310,10 @@ function renderImport(result, validation) {
2796
3310
  step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
2797
3311
  );
2798
3312
  }
2799
- steps.push({ glyph: CHECK, text: `Created ${displayPath2(result.root, result.backup)}` });
3313
+ steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
2800
3314
  const lines = formatSteps(steps);
2801
3315
  if (validation === void 0) {
2802
- lines.push(...formatSteps([skippedValidationStep(result.environments)]));
3316
+ lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
2803
3317
  } else if (validation.ok) {
2804
3318
  lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
2805
3319
  } else {
@@ -2808,7 +3322,7 @@ function renderImport(result, validation) {
2808
3322
  lines.push("", "Done. .penv/ is now your source of truth.");
2809
3323
  return lines;
2810
3324
  }
2811
- var importCommand = (0, import_citty9.defineCommand)({
3325
+ var importCommand = (0, import_citty10.defineCommand)({
2812
3326
  meta: {
2813
3327
  name: "import",
2814
3328
  description: "Import an existing dotenv file; it becomes the source of truth"
@@ -2840,16 +3354,16 @@ var importCommand = (0, import_citty9.defineCommand)({
2840
3354
 
2841
3355
  // src/commands/key.ts
2842
3356
  var import_node_crypto = require("crypto");
2843
- var import_core15 = require("@penvhq/core");
2844
- var import_citty10 = require("citty");
3357
+ var import_core16 = require("@penvhq/core");
3358
+ var import_citty11 = require("citty");
2845
3359
 
2846
3360
  // src/keychain.ts
2847
- var import_node_module = require("module");
3361
+ var import_node_module2 = require("module");
2848
3362
  var import_meta = {};
2849
3363
  var cached;
2850
3364
  function entryConstructor() {
2851
3365
  if (cached === void 0) {
2852
- const require2 = (0, import_node_module.createRequire)(import_meta.url);
3366
+ const require2 = (0, import_node_module2.createRequire)(import_meta.url);
2853
3367
  cached = require2("@napi-rs/keyring").Entry;
2854
3368
  }
2855
3369
  return cached;
@@ -2874,35 +3388,35 @@ function runKeyCreate(options) {
2874
3388
  const environment = targetEnvironment(project, options.environment);
2875
3389
  const declared = project.config.keys?.[environment];
2876
3390
  if (declared === void 0) {
2877
- throw new import_core15.PenvError(
3391
+ throw new import_core16.PenvError(
2878
3392
  "KEY_SOURCE_UNDECLARED",
2879
3393
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
2880
3394
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
2881
3395
  );
2882
3396
  }
2883
- const key = (0, import_node_crypto.randomBytes)(import_core15.KEY_BYTES).toString("base64");
3397
+ const key = (0, import_node_crypto.randomBytes)(import_core16.KEY_BYTES).toString("base64");
2884
3398
  if (declared.source === "keychain") {
2885
3399
  const keychain = options.keychain ?? defaultKeychain;
2886
3400
  if (options.force !== true) {
2887
3401
  let existing;
2888
3402
  try {
2889
- existing = keychain.getPassword(import_core15.KEYCHAIN_SERVICE, declared.id);
3403
+ existing = keychain.getPassword(import_core16.KEYCHAIN_SERVICE, declared.id);
2890
3404
  } catch (cause) {
2891
- throw new import_core15.PenvError(
3405
+ throw new import_core16.PenvError(
2892
3406
  "KEYCHAIN_UNAVAILABLE",
2893
3407
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
2894
3408
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
2895
3409
  );
2896
3410
  }
2897
3411
  if (existing !== null) {
2898
- throw new import_core15.PenvError(
3412
+ throw new import_core16.PenvError(
2899
3413
  "KEY_EXISTS",
2900
3414
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
2901
3415
  "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."
2902
3416
  );
2903
3417
  }
2904
3418
  }
2905
- keychain.setPassword(import_core15.KEYCHAIN_SERVICE, declared.id, key);
3419
+ keychain.setPassword(import_core16.KEYCHAIN_SERVICE, declared.id, key);
2906
3420
  return { source: "keychain", environment, id: declared.id };
2907
3421
  }
2908
3422
  return {
@@ -2913,29 +3427,29 @@ function runKeyCreate(options) {
2913
3427
  key
2914
3428
  };
2915
3429
  }
2916
- function renderKeyCreate(result) {
2917
- if (result.source === "keychain") {
3430
+ function renderKeyCreate(result2) {
3431
+ if (result2.source === "keychain") {
2918
3432
  return [
2919
- `A new key for environment ${result.environment}, stored in your OS keychain as \`${result.id}\`.`,
3433
+ `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
2920
3434
  "",
2921
3435
  "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
2922
3436
  "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
2923
3437
  ];
2924
3438
  }
2925
3439
  return [
2926
- `A new key for environment ${result.environment}. penv did not store it.`,
3440
+ `A new key for environment ${result2.environment}. penv did not store it.`,
2927
3441
  "",
2928
- ` ${result.variable}=${result.key}`,
3442
+ ` ${result2.variable}=${result2.key}`,
2929
3443
  "",
2930
3444
  "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
2931
3445
  "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
2932
3446
  "without it, and penv keeps no copy to fall back on."
2933
3447
  ];
2934
3448
  }
2935
- var keyCommand = (0, import_citty10.defineCommand)({
3449
+ var keyCommand = (0, import_citty11.defineCommand)({
2936
3450
  meta: { name: "key", description: "Work with encryption keys" },
2937
3451
  subCommands: {
2938
- create: (0, import_citty10.defineCommand)({
3452
+ create: (0, import_citty11.defineCommand)({
2939
3453
  meta: { name: "create", description: "Generate a key for an environment" },
2940
3454
  args: {
2941
3455
  env: { type: "string", description: "The environment the key is for" },
@@ -2962,8 +3476,8 @@ var keyCommand = (0, import_citty10.defineCommand)({
2962
3476
  });
2963
3477
 
2964
3478
  // src/commands/list.ts
2965
- var import_core16 = require("@penvhq/core");
2966
- var import_citty11 = require("citty");
3479
+ var import_core17 = require("@penvhq/core");
3480
+ var import_citty12 = require("citty");
2967
3481
  function scopeLabel2(scope) {
2968
3482
  switch (scope.kind) {
2969
3483
  case "environment":
@@ -2975,7 +3489,7 @@ function scopeLabel2(scope) {
2975
3489
  case "unscoped":
2976
3490
  return "default";
2977
3491
  default:
2978
- return (0, import_core16.assertNever)(scope, "scope");
3492
+ return (0, import_core17.assertNever)(scope, "scope");
2979
3493
  }
2980
3494
  }
2981
3495
  async function runList(options) {
@@ -2983,12 +3497,12 @@ async function runList(options) {
2983
3497
  const environment = targetEnvironment(project, options.environment);
2984
3498
  const keys = keySourceFor(project, environment);
2985
3499
  const parameters = [];
2986
- for (const resolution of await (0, import_core16.resolveAll)(environment, project.provider, keys)) {
3500
+ for (const resolution of await (0, import_core17.resolveAll)(environment, project.provider, keys)) {
2987
3501
  const winner = resolution.winner;
2988
3502
  const scope = winner?.file.scope;
2989
3503
  parameters.push({
2990
3504
  parameter: resolution.parameter,
2991
- variable: (0, import_core16.variableName)(resolution.ref, project.config),
3505
+ variable: (0, import_core17.variableName)(resolution.ref, project.config),
2992
3506
  scope: scope === void 0 ? "absent" : scopeLabel2(scope),
2993
3507
  location: winner?.location,
2994
3508
  encrypted: winner?.file.encrypted === true,
@@ -2997,13 +3511,13 @@ async function runList(options) {
2997
3511
  }
2998
3512
  return { environment, parameters };
2999
3513
  }
3000
- function renderList(result) {
3001
- if (result.parameters.length === 0) {
3002
- return [`No parameters in ${PENV_DIR}/ for environment ${result.environment}.`];
3514
+ function renderList(result2) {
3515
+ if (result2.parameters.length === 0) {
3516
+ return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3003
3517
  }
3004
- return columns(result.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3518
+ return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3005
3519
  }
3006
- var listCommand = (0, import_citty11.defineCommand)({
3520
+ var listCommand = (0, import_citty12.defineCommand)({
3007
3521
  meta: { name: "list", description: "List parameters" },
3008
3522
  args: {
3009
3523
  env: { type: "string", description: "The environment to resolve against" },
@@ -3011,18 +3525,18 @@ var listCommand = (0, import_citty11.defineCommand)({
3011
3525
  },
3012
3526
  run({ args }) {
3013
3527
  return guard(async () => {
3014
- const result = await runList({
3528
+ const result2 = await runList({
3015
3529
  cwd: process.cwd(),
3016
3530
  ...args.env === void 0 ? {} : { environment: args.env }
3017
3531
  });
3018
- write(args.json === true ? [JSON.stringify(result, null, 2)] : renderList(result));
3532
+ write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
3019
3533
  });
3020
3534
  }
3021
3535
  });
3022
3536
 
3023
3537
  // src/commands/mv.ts
3024
- var import_core17 = require("@penvhq/core");
3025
- var import_citty12 = require("citty");
3538
+ var import_core18 = require("@penvhq/core");
3539
+ var import_citty13 = require("citty");
3026
3540
  function environmentOf2(file) {
3027
3541
  const scope = file.scope;
3028
3542
  return scope.kind === "environment" || scope.kind === "environment-local" ? scope.environment : void 0;
@@ -3037,38 +3551,39 @@ async function planFile(project, source, target, parameter) {
3037
3551
  }
3038
3552
  const environment = environmentOf2(source);
3039
3553
  if (environment === void 0) {
3040
- throw new import_core17.PenvError(
3554
+ throw new import_core18.PenvError(
3041
3555
  "SECRET_SCOPE_AMBIGUOUS",
3042
- `${PENV_DIR}/${(0, import_core17.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3556
+ `${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`,
3043
3557
  "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."
3044
3558
  );
3045
3559
  }
3046
3560
  const keys = keySourceFor(project, environment);
3047
- const opened = (0, import_core17.openValue)(source, stored, keys);
3561
+ const opened = (0, import_core18.openValue)(source, stored, keys);
3048
3562
  if (opened.kind === "failed") {
3049
- throw new import_core17.PenvError(
3563
+ throw new import_core18.PenvError(
3050
3564
  "VALUE_UNDECRYPTABLE",
3051
- `${PENV_DIR}/${(0, import_core17.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3565
+ `${PENV_DIR}/${(0, import_core18.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3052
3566
  "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."
3053
3567
  );
3054
3568
  }
3055
3569
  return {
3056
3570
  source,
3057
3571
  target,
3058
- contents: (0, import_core17.sealValue)(target, opened.value, keys, parameter, environment),
3572
+ contents: (0, import_core18.sealValue)(target, opened.value, keys, parameter, environment),
3059
3573
  resealed: true
3060
3574
  };
3061
3575
  }
3062
3576
  function filesOf(all, ref) {
3063
- const id = (0, import_core17.parameterId)(ref);
3064
- return all.filter((file) => (0, import_core17.parameterId)(file) === id);
3577
+ const id = (0, import_core18.parameterId)(ref);
3578
+ return all.filter((file) => (0, import_core18.parameterId)(file) === id);
3065
3579
  }
3066
3580
  async function runMove(options) {
3067
3581
  const project = openProject(options.cwd);
3068
3582
  const from = refFromKey(options.from, project.config);
3583
+ assertWritableKey(options.to);
3069
3584
  const to = refFromKey(options.to, project.config);
3070
- if ((0, import_core17.parameterId)(from) === (0, import_core17.parameterId)(to)) {
3071
- throw new import_core17.PenvError(
3585
+ if ((0, import_core18.parameterId)(from) === (0, import_core18.parameterId)(to)) {
3586
+ throw new import_core18.PenvError(
3072
3587
  "PARAMETER_UNCHANGED",
3073
3588
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
3074
3589
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -3078,24 +3593,24 @@ async function runMove(options) {
3078
3593
  const sources = filesOf(all, from);
3079
3594
  const meta = await project.provider.readMeta(from);
3080
3595
  if (sources.length === 0 && meta === void 0) {
3081
- throw new import_core17.PenvError(
3596
+ throw new import_core18.PenvError(
3082
3597
  "PARAMETER_ABSENT",
3083
- `Parameter ${(0, import_core17.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
3598
+ `Parameter ${(0, import_core18.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
3084
3599
  `\`penv list\` shows every parameter penv holds.`
3085
3600
  );
3086
3601
  }
3087
3602
  const occupied2 = filesOf(all, to);
3088
3603
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
3089
- throw new import_core17.PenvError(
3604
+ throw new import_core18.PenvError(
3090
3605
  "PARAMETER_EXISTS",
3091
- `Parameter ${(0, import_core17.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
3092
- `Remove or rename ${(0, import_core17.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
3606
+ `Parameter ${(0, import_core18.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
3607
+ `Remove or rename ${(0, import_core18.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
3093
3608
  );
3094
3609
  }
3095
3610
  const planned = [];
3096
3611
  for (const source of sources) {
3097
3612
  const target = { ...source, namespace: to.namespace, name: to.name };
3098
- const one = await planFile(project, source, target, (0, import_core17.parameterId)(to));
3613
+ const one = await planFile(project, source, target, (0, import_core18.parameterId)(to));
3099
3614
  if (one !== void 0) {
3100
3615
  planned.push(one);
3101
3616
  }
@@ -3113,39 +3628,39 @@ async function runMove(options) {
3113
3628
  await project.provider.removeMeta(from);
3114
3629
  }
3115
3630
  return {
3116
- from: (0, import_core17.parameterId)(from),
3117
- to: (0, import_core17.parameterId)(to),
3631
+ from: (0, import_core18.parameterId)(from),
3632
+ to: (0, import_core18.parameterId)(to),
3118
3633
  files: planned.map((file) => ({
3119
- from: (0, import_core17.formatValueFile)(file.source),
3120
- to: (0, import_core17.formatValueFile)(file.target),
3634
+ from: (0, import_core18.formatValueFile)(file.source),
3635
+ to: (0, import_core18.formatValueFile)(file.target),
3121
3636
  resealed: file.resealed
3122
3637
  })),
3123
3638
  // The meta's path, not the parameter's dotted id: `redis.password` is what
3124
3639
  // the schema calls it and `redis/password.json` is the file, and a report
3125
3640
  // that printed the first while moving the second names no file on disk.
3126
- meta: meta === void 0 ? void 0 : (0, import_core17.formatMetaFile)({ ...to, format: "json" }),
3127
- schema: { was: (0, import_core17.accessPath)(from).join("."), now: (0, import_core17.accessPath)(to).join(".") }
3641
+ meta: meta === void 0 ? void 0 : (0, import_core18.formatMetaFile)({ ...to, format: "json" }),
3642
+ schema: { was: (0, import_core18.accessPath)(from).join("."), now: (0, import_core18.accessPath)(to).join(".") }
3128
3643
  };
3129
3644
  }
3130
- function renderMove(result) {
3131
- const rows = result.files.map((file) => ({
3645
+ function renderMove(result2) {
3646
+ const rows = result2.files.map((file) => ({
3132
3647
  glyph: CHECK,
3133
3648
  label: "Moved",
3134
3649
  subject: `${PENV_DIR}/${file.to}`,
3135
3650
  ...file.resealed ? { detail: "re-sealed for its new address" } : {}
3136
3651
  }));
3137
- if (result.meta !== void 0) {
3138
- rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result.meta}` });
3652
+ if (result2.meta !== void 0) {
3653
+ rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
3139
3654
  }
3140
3655
  const lines = formatRows(rows);
3141
3656
  lines.push(
3142
3657
  "",
3143
- ` .penv/env.ts still declares \`${result.schema.was}\`. Rename it to \`${result.schema.now}\`,`,
3658
+ ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3144
3659
  " or `penv validate` will report the value as unused and the declaration as unset."
3145
3660
  );
3146
3661
  return lines;
3147
3662
  }
3148
- var mvCommand = (0, import_citty12.defineCommand)({
3663
+ var mvCommand = (0, import_citty13.defineCommand)({
3149
3664
  meta: { name: "mv", description: "Rename a parameter, every scope and its meta at once" },
3150
3665
  args: {
3151
3666
  from: {
@@ -3166,9 +3681,86 @@ var mvCommand = (0, import_citty12.defineCommand)({
3166
3681
  }
3167
3682
  });
3168
3683
 
3684
+ // src/commands/pull.ts
3685
+ var import_citty14 = require("citty");
3686
+ async function runPull(options) {
3687
+ const project = openProject(options.cwd);
3688
+ const environment = targetEnvironment(project, options.environment);
3689
+ const source = await sourceProviderFor(project, environment);
3690
+ if (source.type === LOCAL_TREE_TYPE) {
3691
+ return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
3692
+ }
3693
+ const tree = localTree(project);
3694
+ const files = await source.list();
3695
+ let values = 0;
3696
+ for (const file of files) {
3697
+ const value = await source.read(file);
3698
+ if (value === void 0) {
3699
+ continue;
3700
+ }
3701
+ tree.writeSync(file, value);
3702
+ values += 1;
3703
+ }
3704
+ const refs = refsFrom(files);
3705
+ let meta = 0;
3706
+ for (const ref of refs) {
3707
+ const block = await source.readMeta(ref);
3708
+ if (block === void 0) {
3709
+ continue;
3710
+ }
3711
+ tree.writeMetaSync(ref, block);
3712
+ meta += 1;
3713
+ }
3714
+ return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
3715
+ }
3716
+ function renderPull(result2) {
3717
+ if (result2.localSource) {
3718
+ return formatRows([
3719
+ {
3720
+ glyph: CHECK,
3721
+ label: "Nothing to pull",
3722
+ subject: `environment ${result2.environment} has no separate source of truth`,
3723
+ detail: "its values live in the local .penv tree already"
3724
+ }
3725
+ ]);
3726
+ }
3727
+ return formatRows([
3728
+ {
3729
+ glyph: CHECK,
3730
+ label: "Pulled",
3731
+ subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
3732
+ detail: `from the ${result2.source} provider for environment ${result2.environment}`
3733
+ },
3734
+ {
3735
+ glyph: CHECK,
3736
+ label: "Parameters",
3737
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
3738
+ detail: "written into the local .penv tree"
3739
+ }
3740
+ ]);
3741
+ }
3742
+ var pullCommand = (0, import_citty14.defineCommand)({
3743
+ meta: {
3744
+ name: "pull",
3745
+ description: "Materialise the local .penv tree from an environment's source-of-truth provider"
3746
+ },
3747
+ args: {
3748
+ env: { type: "string", description: "The environment to pull" }
3749
+ },
3750
+ run({ args }) {
3751
+ return guard(async () => {
3752
+ const result2 = await runPull({
3753
+ cwd: process.cwd(),
3754
+ ...args.env === void 0 ? {} : { environment: args.env }
3755
+ });
3756
+ write(renderPull(result2));
3757
+ });
3758
+ }
3759
+ });
3760
+
3169
3761
  // src/commands/remove.ts
3170
- var import_core18 = require("@penvhq/core");
3171
- var import_citty13 = require("citty");
3762
+ var import_core19 = require("@penvhq/core");
3763
+ var import_citty15 = require("citty");
3172
3764
  async function runRemove(options) {
3173
3765
  const project = openProject(options.cwd);
3174
3766
  const ref = refFromKey(options.key);
@@ -3185,17 +3777,17 @@ async function runRemove(options) {
3185
3777
  continue;
3186
3778
  }
3187
3779
  await project.provider.remove(file);
3188
- removed.push((0, import_core18.formatValueFile)(file));
3780
+ removed.push((0, import_core19.formatValueFile)(file));
3189
3781
  }
3190
3782
  return {
3191
3783
  parameter: options.key,
3192
3784
  removed,
3193
- considered: files.map((file) => (0, import_core18.formatValueFile)(file))
3785
+ considered: files.map((file) => (0, import_core19.formatValueFile)(file))
3194
3786
  };
3195
3787
  }
3196
- function renderRemove(result) {
3197
- if (result.removed.length === 0) {
3198
- const first = result.considered[0] ?? result.parameter;
3788
+ function renderRemove(result2) {
3789
+ if (result2.removed.length === 0) {
3790
+ const first = result2.considered[0] ?? result2.parameter;
3199
3791
  return formatRows([
3200
3792
  {
3201
3793
  glyph: WARN,
@@ -3205,14 +3797,14 @@ function renderRemove(result) {
3205
3797
  }
3206
3798
  ]);
3207
3799
  }
3208
- const rows = result.removed.map((location) => ({
3800
+ const rows = result2.removed.map((location) => ({
3209
3801
  glyph: CHECK,
3210
3802
  label: "Removed",
3211
3803
  subject: `${PENV_DIR}/${location}`
3212
3804
  }));
3213
3805
  return formatRows(rows);
3214
3806
  }
3215
- var removeCommand = (0, import_citty13.defineCommand)({
3807
+ var removeCommand = (0, import_citty15.defineCommand)({
3216
3808
  meta: { name: "remove", description: "Delete a parameter" },
3217
3809
  args: {
3218
3810
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -3238,15 +3830,195 @@ var removeCommand = (0, import_citty13.defineCommand)({
3238
3830
  }
3239
3831
  });
3240
3832
 
3833
+ // src/commands/rotate.ts
3834
+ var import_core20 = require("@penvhq/core");
3835
+ var import_citty16 = require("citty");
3836
+ function rotatingFile(ref, environment) {
3837
+ return {
3838
+ namespace: ref.namespace,
3839
+ name: ref.name,
3840
+ scope: { kind: "environment", environment },
3841
+ encrypted: false
3842
+ };
3843
+ }
3844
+ async function writeRotatedValue(project, provider, ref, environment, value) {
3845
+ if (provider.type === LOCAL_TREE_TYPE) {
3846
+ await sealAwareWrite({
3847
+ project,
3848
+ provider,
3849
+ ref,
3850
+ scope: { kind: "environment", environment },
3851
+ value,
3852
+ environment
3853
+ });
3854
+ return;
3855
+ }
3856
+ await provider.write(rotatingFile(ref, environment), value);
3857
+ }
3858
+ function requireNewValue(value, phase, key) {
3859
+ if (value === void 0) {
3860
+ throw new import_core20.PenvError(
3861
+ "ROTATION_NO_VALUE",
3862
+ `A ${phase} rotation of ${key} writes a new value, and none was given`,
3863
+ "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
3864
+ );
3865
+ }
3866
+ return value;
3867
+ }
3868
+ function requireRetaining(provider, environment) {
3869
+ if (!(0, import_core20.retainsPrevious)(provider)) {
3870
+ throw new import_core20.PenvError(
3871
+ "ROTATION_NOT_RETAINING",
3872
+ `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`,
3873
+ "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."
3874
+ );
3875
+ }
3876
+ return provider;
3877
+ }
3878
+ async function runRotate(options) {
3879
+ const project = openProject(options.cwd);
3880
+ const environment = targetEnvironment(project, options.environment);
3881
+ const ref = refFromKey(options.key, project.config);
3882
+ const provider = await sourceProviderFor(project, environment);
3883
+ const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
3884
+ const before = await provider.readMeta(ref);
3885
+ const { mechanism } = (0, import_core20.rotationOf)(before, environment);
3886
+ if (mechanism === void 0) {
3887
+ throw new import_core20.PenvError(
3888
+ "ROTATION_NO_MECHANISM",
3889
+ `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
3890
+ '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.'
3891
+ );
3892
+ }
3893
+ const begin = options.begin === true;
3894
+ const complete = options.complete === true;
3895
+ if (mechanism === "atomic-cutover") {
3896
+ if (begin || complete) {
3897
+ throw new import_core20.PenvError(
3898
+ "ROTATION_MECHANISM_MISMATCH",
3899
+ `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
3900
+ "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."
3901
+ );
3902
+ }
3903
+ const value = requireNewValue(options.value, "cutover", options.key);
3904
+ await writeRotatedValue(project, provider, ref, environment, value);
3905
+ const after2 = (0, import_core20.completeRotation)(before, environment, nowIso);
3906
+ await provider.writeMeta(ref, after2);
3907
+ return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
3908
+ }
3909
+ const retaining = requireRetaining(provider, environment);
3910
+ if (begin === complete) {
3911
+ throw new import_core20.PenvError(
3912
+ "ROTATION_PHASE_REQUIRED",
3913
+ `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
3914
+ "`--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."
3915
+ );
3916
+ }
3917
+ if (begin) {
3918
+ const value = requireNewValue(options.value, "begin", options.key);
3919
+ await writeRotatedValue(project, retaining, ref, environment, value);
3920
+ const after2 = (0, import_core20.beginRotation)(before, environment, nowIso);
3921
+ await retaining.writeMeta(ref, after2);
3922
+ return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
3923
+ }
3924
+ const after = (0, import_core20.completeRotation)(before, environment, nowIso);
3925
+ await retaining.writeMeta(ref, after);
3926
+ return result(ref, environment, mechanism, "complete", retaining.type, false, after);
3927
+ }
3928
+ function result(ref, environment, mechanism, phase, source, wroteValue, after) {
3929
+ const { state, rotatingSince, lastRotated } = (0, import_core20.rotationOf)(after, environment);
3930
+ return {
3931
+ parameter: [...ref.namespace, ref.name].join("/"),
3932
+ environment,
3933
+ mechanism,
3934
+ phase,
3935
+ source,
3936
+ wroteValue,
3937
+ state,
3938
+ rotatingSince,
3939
+ lastRotated
3940
+ };
3941
+ }
3942
+ function renderRotate(result2) {
3943
+ if (result2.phase === "begin") {
3944
+ return formatRows([
3945
+ {
3946
+ glyph: CHECK,
3947
+ label: "Rotating",
3948
+ subject: result2.parameter,
3949
+ detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
3950
+ },
3951
+ {
3952
+ glyph: CHECK,
3953
+ label: "Previous",
3954
+ subject: "still readable",
3955
+ detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
3956
+ }
3957
+ ]);
3958
+ }
3959
+ if (result2.phase === "complete") {
3960
+ return formatRows([
3961
+ {
3962
+ glyph: CHECK,
3963
+ label: "Rotated",
3964
+ subject: result2.parameter,
3965
+ detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
3966
+ }
3967
+ ]);
3968
+ }
3969
+ return formatRows([
3970
+ {
3971
+ glyph: CHECK,
3972
+ label: "Rotated",
3973
+ subject: result2.parameter,
3974
+ detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
3975
+ }
3976
+ ]);
3977
+ }
3978
+ var rotateCommand = (0, import_citty16.defineCommand)({
3979
+ meta: {
3980
+ name: "rotate",
3981
+ description: "Rotate a parameter's value by the mechanism its meta declares"
3982
+ },
3983
+ args: {
3984
+ key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
3985
+ value: {
3986
+ type: "positional",
3987
+ required: false,
3988
+ description: "The new value; read from stdin if omitted. Not needed with --complete"
3989
+ },
3990
+ env: { type: "string", description: "The environment to rotate in" },
3991
+ begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
3992
+ complete: { type: "boolean", description: "Close a dual-valid grace window" }
3993
+ },
3994
+ run({ args }) {
3995
+ return guard(async () => {
3996
+ const value = args.complete === true ? void 0 : args.value ?? await readStdin();
3997
+ write(
3998
+ renderRotate(
3999
+ await runRotate({
4000
+ cwd: process.cwd(),
4001
+ key: args.key,
4002
+ ...value === void 0 ? {} : { value },
4003
+ ...args.env === void 0 ? {} : { environment: args.env },
4004
+ ...args.begin === void 0 ? {} : { begin: args.begin },
4005
+ ...args.complete === void 0 ? {} : { complete: args.complete }
4006
+ })
4007
+ )
4008
+ );
4009
+ });
4010
+ }
4011
+ });
4012
+
3241
4013
  // src/commands/watch.ts
3242
4014
  var import_node_fs5 = require("fs");
3243
- var import_node_path7 = require("path");
3244
- var import_core19 = require("@penvhq/core");
3245
- var import_citty14 = require("citty");
4015
+ var import_node_path8 = require("path");
4016
+ var import_core21 = require("@penvhq/core");
4017
+ var import_citty17 = require("citty");
3246
4018
  var DEBOUNCE_MS = 100;
3247
4019
  function runWatch(options) {
3248
4020
  const project = openProject(options.cwd);
3249
- const configFile = (0, import_node_path7.basename)(project.configFile);
4021
+ const configFile = (0, import_node_path8.basename)(project.configFile);
3250
4022
  const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
3251
4023
  const watchers = /* @__PURE__ */ new Set();
3252
4024
  let timer;
@@ -3263,12 +4035,12 @@ function runWatch(options) {
3263
4035
  }
3264
4036
  running = true;
3265
4037
  try {
3266
- const result = await runValidate({
4038
+ const result2 = await runValidate({
3267
4039
  cwd: options.cwd,
3268
4040
  ...options.environment === void 0 ? {} : { environment: options.environment }
3269
4041
  });
3270
4042
  if (!closed) {
3271
- options.onResult?.(result);
4043
+ options.onResult?.(result2);
3272
4044
  }
3273
4045
  } catch (error) {
3274
4046
  if (!closed) {
@@ -3299,8 +4071,8 @@ function runWatch(options) {
3299
4071
  watcher.close();
3300
4072
  }
3301
4073
  function armRecovery(target, recursive, only) {
3302
- const parent = (0, import_node_path7.dirname)(target);
3303
- const name = (0, import_node_path7.basename)(target);
4074
+ const parent = (0, import_node_path8.dirname)(target);
4075
+ const name = (0, import_node_path8.basename)(target);
3304
4076
  let recovery;
3305
4077
  try {
3306
4078
  recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
@@ -3311,7 +4083,7 @@ function runWatch(options) {
3311
4083
  stop(recovery);
3312
4084
  return;
3313
4085
  }
3314
- if (filename !== null && (0, import_node_path7.basename)(filename) !== name) {
4086
+ if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
3315
4087
  return;
3316
4088
  }
3317
4089
  if (!(0, import_node_fs5.existsSync)(target)) {
@@ -3346,7 +4118,7 @@ function runWatch(options) {
3346
4118
  schedule();
3347
4119
  return;
3348
4120
  }
3349
- if (only !== void 0 && (filename === null || (0, import_node_path7.basename)(filename) !== only)) {
4121
+ if (only !== void 0 && (filename === null || (0, import_node_path8.basename)(filename) !== only)) {
3350
4122
  return;
3351
4123
  }
3352
4124
  schedule();
@@ -3373,10 +4145,10 @@ function runWatch(options) {
3373
4145
  watchers.add(watcher);
3374
4146
  }
3375
4147
  addWatcher(project.penvDir, true);
3376
- addWatcher((0, import_node_path7.dirname)(project.configFile), false, configFile);
3377
- if ((0, import_core19.schemaInsideTree)(project.config) === void 0) {
3378
- const schemaFile = (0, import_node_path7.resolve)(project.root, (0, import_core19.schemaFileOf)(project.config));
3379
- addWatcher((0, import_node_path7.dirname)(schemaFile), false, (0, import_node_path7.basename)(schemaFile));
4148
+ addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
4149
+ if ((0, import_core21.schemaInsideTree)(project.config) === void 0) {
4150
+ const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core21.schemaFileOf)(project.config));
4151
+ addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
3380
4152
  }
3381
4153
  void validate();
3382
4154
  return {
@@ -3392,8 +4164,8 @@ function runWatch(options) {
3392
4164
  }
3393
4165
  };
3394
4166
  }
3395
- function renderWatch(result) {
3396
- return ["", ...renderValidate(result), ...renderDrift(result.drift, result.environment)];
4167
+ function renderWatch(result2) {
4168
+ return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
3397
4169
  }
3398
4170
  function renderDrift(drift, environment) {
3399
4171
  const rows = [
@@ -3419,7 +4191,7 @@ function renderDrift(drift, environment) {
3419
4191
  }
3420
4192
  return lines;
3421
4193
  }
3422
- var watchCommand = (0, import_citty14.defineCommand)({
4194
+ var watchCommand = (0, import_citty17.defineCommand)({
3423
4195
  meta: {
3424
4196
  name: "watch",
3425
4197
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -3432,8 +4204,8 @@ var watchCommand = (0, import_citty14.defineCommand)({
3432
4204
  const handle = runWatch({
3433
4205
  cwd: process.cwd(),
3434
4206
  ...args.env === void 0 ? {} : { environment: args.env },
3435
- onResult: (result) => {
3436
- write(renderWatch(result));
4207
+ onResult: (result2) => {
4208
+ write(renderWatch(result2));
3437
4209
  },
3438
4210
  // A failing cycle is reported and watching continues. `reportError`
3439
4211
  // marks the process failed, which `validate` wants and a loop does not:
@@ -3447,10 +4219,10 @@ var watchCommand = (0, import_citty14.defineCommand)({
3447
4219
  }
3448
4220
  });
3449
4221
  write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
3450
- await new Promise((resolve6) => {
4222
+ await new Promise((resolve7) => {
3451
4223
  process.once("SIGINT", () => {
3452
4224
  handle.close();
3453
- resolve6();
4225
+ resolve7();
3454
4226
  });
3455
4227
  });
3456
4228
  });
@@ -3458,7 +4230,7 @@ var watchCommand = (0, import_citty14.defineCommand)({
3458
4230
  });
3459
4231
 
3460
4232
  // src/index.ts
3461
- var main = (0, import_citty15.defineCommand)({
4233
+ var main = (0, import_citty18.defineCommand)({
3462
4234
  meta: {
3463
4235
  name: "penv",
3464
4236
  description: "Configuration that shares a data model with your production secret manager"
@@ -3469,8 +4241,11 @@ var main = (0, import_citty15.defineCommand)({
3469
4241
  generate: generateCommand,
3470
4242
  get: getCommand,
3471
4243
  set: setCommand,
4244
+ fill: fillCommand,
3472
4245
  mv: mvCommand,
4246
+ pull: pullCommand,
3473
4247
  push: pushCommand,
4248
+ rotate: rotateCommand,
3474
4249
  remove: removeCommand,
3475
4250
  list: listCommand,
3476
4251
  encrypt: encryptCommand,
@@ -3482,8 +4257,8 @@ var main = (0, import_citty15.defineCommand)({
3482
4257
  }
3483
4258
  });
3484
4259
  function runMain() {
3485
- (0, import_core20.setKeychain)(defaultKeychain);
3486
- return (0, import_citty15.runMain)(main);
4260
+ (0, import_core22.setKeychain)(defaultKeychain);
4261
+ return (0, import_citty18.runMain)(main);
3487
4262
  }
3488
4263
  // Annotate the CommonJS export names for ESM import in node:
3489
4264
  0 && (module.exports = {
@@ -3493,21 +4268,27 @@ function runMain() {
3493
4268
  insertEnvAlias,
3494
4269
  main,
3495
4270
  renderDoctor,
4271
+ renderFill,
3496
4272
  renderMove,
4273
+ renderPull,
3497
4274
  renderPush,
4275
+ renderRotate,
3498
4276
  renderWatch,
3499
4277
  runDecrypt,
3500
4278
  runDoctor,
3501
4279
  runEncrypt,
3502
4280
  runExplain,
4281
+ runFill,
3503
4282
  runGenerate,
3504
4283
  runGet,
3505
4284
  runInit,
3506
4285
  runList,
3507
4286
  runMain,
3508
4287
  runMove,
4288
+ runPull,
3509
4289
  runPush,
3510
4290
  runRemove,
4291
+ runRotate,
3511
4292
  runSet,
3512
4293
  runValidate,
3513
4294
  runWatch