@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.js CHANGED
@@ -1,43 +1,76 @@
1
1
  // src/index.ts
2
2
  import { setKeychain } from "@penvhq/core";
3
- import { runMain as cittyRunMain, defineCommand as defineCommand15 } from "citty";
3
+ import { runMain as cittyRunMain, defineCommand as defineCommand18 } from "citty";
4
4
 
5
5
  // src/commands/doctor.ts
6
6
  import {
7
7
  accessPath as accessPath3,
8
+ assertNever,
8
9
  effectiveMeta,
10
+ formatValueFile as formatValueFile2,
9
11
  isPublicVariable,
10
12
  isRequired,
11
13
  isSecret,
14
+ isStuck,
15
+ openValue as openValue2,
12
16
  resolveAll as resolveAll2,
17
+ rotationOf,
18
+ tryParseDuration,
13
19
  variableName as variableName3
14
20
  } from "@penvhq/core";
15
21
  import { createGithubSink as createGithubSink2 } from "@penvhq/sink-github";
16
22
  import { defineCommand as defineCommand3 } from "citty";
17
23
 
18
24
  // src/project.ts
19
- import { dirname, resolve } from "path";
25
+ import { dirname as dirname2, resolve as resolve2 } from "path";
20
26
  import {
21
27
  candidatesFor,
22
28
  formatValueFile,
29
+ isCanonicalSegment,
23
30
  isReservedToken,
24
31
  loadConfig,
25
32
  openValue,
26
33
  PenvError as PenvError2,
27
34
  parameterId,
28
35
  ReservedTokenError,
36
+ refFromAccessPath,
29
37
  resolveEnvironment,
30
38
  resolveKeySource
31
39
  } from "@penvhq/core";
32
40
  import { FilesystemProvider } from "@penvhq/provider-filesystem";
33
41
 
34
42
  // src/registry.ts
43
+ import { createRequire } from "module";
44
+ import { dirname, resolve } from "path";
45
+ import { pathToFileURL } from "url";
35
46
  import { PenvError } from "@penvhq/core";
36
47
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
48
+ import { createKubernetesProvider } from "@penvhq/provider-kubernetes";
49
+ import { createMockProvider } from "@penvhq/provider-mock";
50
+ import { createSsmProvider } from "@penvhq/provider-ssm";
51
+ import { createVaultProvider } from "@penvhq/provider-vault";
52
+ var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
37
53
  var LOCAL_TREE_TYPE = "filesystem";
38
54
  var REGISTRY = /* @__PURE__ */ new Map([
39
- [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })]
55
+ [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
56
+ ["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
57
+ ["ssm", ({ providerConfig }) => createSsmProvider({ path: providerConfig?.path ?? "penv" })],
58
+ [
59
+ "kubernetes",
60
+ ({ providerConfig }) => createKubernetesProvider(kubernetesOptions(providerConfig))
61
+ ],
62
+ ["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
40
63
  ]);
64
+ var CONTRACT_METHODS = [
65
+ "read",
66
+ "write",
67
+ "list",
68
+ "remove",
69
+ "readMeta",
70
+ "writeMeta",
71
+ "removeMeta"
72
+ ];
73
+ var pluginModuleCache = /* @__PURE__ */ new Map();
41
74
  function isProviderRegistered(type) {
42
75
  return REGISTRY.has(type);
43
76
  }
@@ -48,20 +81,102 @@ function createProvider(type, context) {
48
81
  }
49
82
  return factory(context);
50
83
  }
51
- function assertProvidersRegistered(config) {
84
+ async function createSourceProvider(type, context) {
85
+ if (REGISTRY.has(type)) {
86
+ return createProvider(type, context);
87
+ }
88
+ return loadPluginProvider(type, context);
89
+ }
90
+ async function loadPluginProvider(type, context) {
91
+ const fromDir = resolutionBase(context);
92
+ const specifier = pluginSpecifier(context.providerConfig, type);
93
+ const resolved = resolvePlugin(specifier, fromDir);
94
+ if (resolved === void 0) {
95
+ throw unknownProvider(type, context.environment, specifier);
96
+ }
97
+ let mod;
98
+ try {
99
+ mod = await importPlugin(resolved);
100
+ } catch {
101
+ throw new PenvError(
102
+ "PROVIDER_PLUGIN_LOAD",
103
+ `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
104
+ "It resolved but threw while importing. Check it builds and its dependencies are installed."
105
+ );
106
+ }
107
+ const factory = mod[PLUGIN_FACTORY_EXPORT];
108
+ if (typeof factory !== "function") {
109
+ throw new PenvError(
110
+ "PROVIDER_PLUGIN_INVALID",
111
+ `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
112
+ `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
113
+ );
114
+ }
115
+ const provider = await factory(context);
116
+ assertSatisfiesContract(provider, specifier);
117
+ return provider;
118
+ }
119
+ function assertProvidersRegistered(config, projectRoot) {
52
120
  for (const [environment, provider] of Object.entries(config.providers)) {
53
- if (!isProviderRegistered(provider.type)) {
54
- throw unknownProvider(provider.type, environment);
121
+ if (isProviderRegistered(provider.type)) {
122
+ continue;
123
+ }
124
+ const specifier = pluginSpecifier(provider, provider.type);
125
+ if (resolvePlugin(specifier, projectRoot) === void 0) {
126
+ throw unknownProvider(provider.type, environment, specifier);
55
127
  }
56
128
  }
57
129
  }
58
- function unknownProvider(type, environment) {
130
+ function pluginSpecifier(providerConfig, type) {
131
+ return providerConfig?.module ?? `@penvhq/provider-${type}`;
132
+ }
133
+ function resolutionBase(context) {
134
+ return dirname(context.root);
135
+ }
136
+ function resolvePlugin(specifier, fromDir) {
137
+ try {
138
+ const require2 = createRequire(resolve(fromDir, "noop.js"));
139
+ return require2.resolve(specifier);
140
+ } catch {
141
+ return void 0;
142
+ }
143
+ }
144
+ function importPlugin(resolvedPath) {
145
+ const cached2 = pluginModuleCache.get(resolvedPath);
146
+ if (cached2 !== void 0) {
147
+ return cached2;
148
+ }
149
+ const loading = import(pathToFileURL(resolvedPath).href);
150
+ pluginModuleCache.set(resolvedPath, loading);
151
+ return loading;
152
+ }
153
+ function assertSatisfiesContract(provider, specifier) {
154
+ for (const method of CONTRACT_METHODS) {
155
+ if (typeof provider[method] !== "function") {
156
+ throw new PenvError(
157
+ "PROVIDER_PLUGIN_INVALID",
158
+ `The provider from \`${specifier}\` is missing \`${method}()\``,
159
+ "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
160
+ );
161
+ }
162
+ }
163
+ }
164
+ function kubernetesOptions(providerConfig) {
165
+ const path = providerConfig?.path ?? "penv";
166
+ const slash = path.indexOf("/");
167
+ if (slash === -1) return { secretName: path };
168
+ const namespace = path.slice(0, slash);
169
+ const secretName = path.slice(slash + 1) || "penv";
170
+ return namespace === "" ? { secretName } : { namespace, secretName };
171
+ }
172
+ function unknownProvider(type, environment, specifier) {
59
173
  const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
60
174
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
175
+ const remedy = specifier === void 0 ? `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.` : `Install its package with \`npm i ${specifier}\`, or name a built-in provider: ${known}.`;
61
176
  return new PenvError(
62
177
  "UNKNOWN_PROVIDER",
63
178
  `The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
64
- `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.`
179
+ remedy
65
180
  );
66
181
  }
67
182
 
@@ -69,9 +184,9 @@ function unknownProvider(type, environment) {
69
184
  var PENV_DIR = ".penv";
70
185
  function openProject(cwd) {
71
186
  const { config, file } = loadConfig(cwd);
72
- const root = dirname(file);
73
- const penvDir = resolve(root, PENV_DIR);
74
- assertProvidersRegistered(config);
187
+ const root = dirname2(file);
188
+ const penvDir = resolve2(root, PENV_DIR);
189
+ assertProvidersRegistered(config, root);
75
190
  return {
76
191
  root,
77
192
  configFile: file,
@@ -90,6 +205,18 @@ function localTree(project) {
90
205
  }
91
206
  return project.provider;
92
207
  }
208
+ async function sourceProviderFor(project, environment) {
209
+ const providerConfig = project.config.providers[environment];
210
+ if (providerConfig === void 0) {
211
+ return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
212
+ }
213
+ return createSourceProvider(providerConfig.type, {
214
+ root: project.penvDir,
215
+ config: project.config,
216
+ providerConfig,
217
+ environment
218
+ });
219
+ }
93
220
  function targetEnvironment(project, explicit) {
94
221
  return resolveEnvironment(project.config, explicit);
95
222
  }
@@ -110,6 +237,26 @@ function refFromKey(key, config) {
110
237
  }
111
238
  return { namespace: segments.slice(0, -1), name };
112
239
  }
240
+ function assertWritableKey(key) {
241
+ const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
242
+ if (segments.length === 0 || segments.every((s) => isCanonicalSegment(s))) {
243
+ return;
244
+ }
245
+ const ref = refFromAccessPath(segments);
246
+ if (ref !== void 0) {
247
+ const suggestion = [...ref.namespace, ref.name].join("/");
248
+ throw new PenvError2(
249
+ "PARAMETER_KEY_CASING",
250
+ `\`${key}\` is not a canonical parameter name`,
251
+ `Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
252
+ );
253
+ }
254
+ throw new PenvError2(
255
+ "PARAMETER_KEY_UNREACHABLE",
256
+ `No value file can be named that reaches \`${key}\``,
257
+ "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."
258
+ );
259
+ }
113
260
  function keySourceFor(project, environment) {
114
261
  return resolveKeySource(project.config, environment);
115
262
  }
@@ -156,7 +303,7 @@ import {
156
303
  accessPath,
157
304
  isReservedToken as isReservedToken2,
158
305
  parameterId as parameterId2,
159
- refFromAccessPath,
306
+ refFromAccessPath as refFromAccessPath2,
160
307
  variableName
161
308
  } from "@penvhq/core";
162
309
  function defOf(node) {
@@ -264,7 +411,7 @@ function computeDrift(input) {
264
411
  continue;
265
412
  }
266
413
  const path = leaf.path.join(".");
267
- const ref = refFromAccessPath(leaf.path);
414
+ const ref = refFromAccessPath2(leaf.path);
268
415
  if (ref === void 0 || isReservedToken2(ref.name, config)) {
269
416
  declared.push({
270
417
  subject: path,
@@ -471,36 +618,36 @@ async function runPush(options) {
471
618
  decrypted: outbound.filter((item) => item.encrypted).length
472
619
  };
473
620
  }
474
- function renderPush(result) {
475
- if (result.pushed === 0) {
621
+ function renderPush(result2) {
622
+ if (result2.pushed === 0) {
476
623
  return formatRows([
477
624
  {
478
625
  glyph: CHECK,
479
626
  label: "Nothing to push",
480
- subject: `no values resolve for environment ${result.environment}`
627
+ subject: `no values resolve for environment ${result2.environment}`
481
628
  }
482
629
  ]);
483
630
  }
484
- const target = `GitHub Actions for environment ${result.environment}${result.repo === void 0 ? "" : ` (${result.repo})`}`;
631
+ const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
485
632
  const rows = [
486
633
  {
487
634
  glyph: CHECK,
488
635
  label: "Pushed",
489
- subject: `${result.pushed} ${result.pushed === 1 ? "secret" : "secrets"}`,
636
+ subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
490
637
  detail: `to ${target}`
491
638
  },
492
639
  {
493
640
  glyph: CHECK,
494
641
  label: "Scopes",
495
- subject: `${result.environmentSecrets} environment, ${result.repositorySecrets} repository`,
642
+ subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
496
643
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
497
644
  }
498
645
  ];
499
- if (result.decrypted > 0) {
646
+ if (result2.decrypted > 0) {
500
647
  rows.push({
501
648
  glyph: WARN,
502
649
  label: "Decrypted",
503
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
650
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
504
651
  detail: "sent as plaintext for GitHub to re-seal under its own key"
505
652
  });
506
653
  }
@@ -520,19 +667,19 @@ var pushCommand = defineCommand({
520
667
  },
521
668
  run({ args }) {
522
669
  return guard(async () => {
523
- const result = await runPush({
670
+ const result2 = await runPush({
524
671
  cwd: process.cwd(),
525
672
  ...args.env === void 0 ? {} : { environment: args.env },
526
673
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
527
674
  });
528
- write(renderPush(result));
675
+ write(renderPush(result2));
529
676
  });
530
677
  }
531
678
  });
532
679
 
533
680
  // src/commands/validate.ts
534
681
  import { resolve as resolvePath } from "path";
535
- import { pathToFileURL } from "url";
682
+ import { pathToFileURL as pathToFileURL2 } from "url";
536
683
  import {
537
684
  accessPath as accessPath2,
538
685
  checkNameCollisions as checkNameCollisions2,
@@ -610,16 +757,16 @@ function validationIssuesOf(cause, schemaPath) {
610
757
  }
611
758
  var schemaLoads = Promise.resolve();
612
759
  function exclusively(work) {
613
- const result = schemaLoads.then(work, work);
614
- schemaLoads = result.then(
760
+ const result2 = schemaLoads.then(work, work);
761
+ schemaLoads = result2.then(
615
762
  () => void 0,
616
763
  () => void 0
617
764
  );
618
- return result;
765
+ return result2;
619
766
  }
620
767
  function loadSchema(project, environment) {
621
768
  const schemaPath = schemaFileOf(project.config);
622
- const file = pathToFileURL(resolvePath(project.root, schemaPath)).href;
769
+ const file = pathToFileURL2(resolvePath(project.root, schemaPath)).href;
623
770
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
624
771
  }
625
772
  async function loadSchemaExclusively(file, schemaPath, environment) {
@@ -721,9 +868,9 @@ async function runValidate(options) {
721
868
  }
722
869
  }
723
870
  drift = computeDrift({ schema, resolutions, config: project.config, environment });
724
- const result = schema.safeParse(object);
725
- if (!result.success) {
726
- for (const problem of result.error.issues) {
871
+ const result2 = schema.safeParse(object);
872
+ if (!result2.success) {
873
+ for (const problem of result2.error.issues) {
727
874
  const path = problem.path.join(".");
728
875
  if (undecryptable.has(path)) {
729
876
  continue;
@@ -739,24 +886,24 @@ async function runValidate(options) {
739
886
  }
740
887
  return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
741
888
  }
742
- function renderValidate(result) {
743
- if (result.ok) {
889
+ function renderValidate(result2) {
890
+ if (result2.ok) {
744
891
  return formatRows([
745
892
  {
746
893
  glyph: CHECK,
747
894
  label: "Schema valid",
748
- subject: `${result.parameters} parameters for environment ${result.environment}`
895
+ subject: `${result2.parameters} parameters for environment ${result2.environment}`
749
896
  }
750
897
  ]);
751
898
  }
752
- const rows = result.issues.map((issue) => ({
899
+ const rows = result2.issues.map((issue) => ({
753
900
  glyph: WARN,
754
901
  label: LABELS[issue.kind],
755
902
  subject: issue.subject,
756
903
  detail: issue.message
757
904
  }));
758
905
  const lines = formatRows(rows);
759
- const remedies = [...new Set(result.issues.map((issue) => issue.remedy))];
906
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
760
907
  for (const remedy of remedies) {
761
908
  if (remedy !== void 0) {
762
909
  lines.push(` ${remedy}`);
@@ -774,12 +921,12 @@ var validateCommand = defineCommand2({
774
921
  },
775
922
  run({ args }) {
776
923
  return guard(async () => {
777
- const result = await runValidate({
924
+ const result2 = await runValidate({
778
925
  cwd: process.cwd(),
779
926
  ...args.env === void 0 ? {} : { environment: args.env }
780
927
  });
781
- write(renderValidate(result));
782
- if (!result.ok) {
928
+ write(renderValidate(result2));
929
+ if (!result2.ok) {
783
930
  process.exitCode = 1;
784
931
  }
785
932
  });
@@ -787,6 +934,7 @@ var validateCommand = defineCommand2({
787
934
  });
788
935
 
789
936
  // src/commands/doctor.ts
937
+ var STUCK_THRESHOLD_MS = 864e5;
790
938
  function skipped(check, label) {
791
939
  return {
792
940
  check,
@@ -800,6 +948,8 @@ async function runDoctor(options) {
800
948
  const project = openProject(options.cwd);
801
949
  const environment = targetEnvironment(project, options.environment);
802
950
  const findings = [];
951
+ const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
952
+ const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
803
953
  const { schema, issues } = await loadSchema(project, environment);
804
954
  if (schema !== void 0) {
805
955
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -848,6 +998,14 @@ async function runDoctor(options) {
848
998
  findings.push(...plaintextSecretFindings(subjects, environment));
849
999
  findings.push(...publicSecretFindings(subjects, environment, project.config));
850
1000
  findings.push(...encryptionFindings(subjects, environment));
1001
+ const rotation = await rotationSubjects(project, environment, subjects, options.source);
1002
+ if (rotation.kind === "unreachable") {
1003
+ findings.push(rotation.finding);
1004
+ } else {
1005
+ findings.push(...overdueFindings(rotation.subjects, environment, now));
1006
+ findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
1007
+ }
1008
+ findings.push(...await providerDriftFindings(project, environment, options.source));
851
1009
  findings.push(...await sinkFindings(project, environment, options.sink));
852
1010
  findings.push({
853
1011
  check: "provider",
@@ -1265,6 +1423,246 @@ async function sinkFindings(project, environment, override) {
1265
1423
  });
1266
1424
  return findings;
1267
1425
  }
1426
+ function humanizeMs(ms) {
1427
+ const abs = Math.max(0, ms);
1428
+ const day = 864e5;
1429
+ const hour = 36e5;
1430
+ const minute = 6e4;
1431
+ const round = (value, unit) => {
1432
+ const n = Math.round(value);
1433
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
1434
+ };
1435
+ if (abs >= day) return round(abs / day, "day");
1436
+ if (abs >= hour) return round(abs / hour, "hour");
1437
+ if (abs >= minute) return round(abs / minute, "minute");
1438
+ return round(abs / 1e3, "second");
1439
+ }
1440
+ function refPathOf(ref) {
1441
+ return [...ref.namespace, ref.name].join("/");
1442
+ }
1443
+ async function rotationSubjects(project, environment, local, override) {
1444
+ const providerConfig = project.config.providers[environment];
1445
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
1446
+ return { kind: "read", subjects: local };
1447
+ }
1448
+ const source = override ?? await sourceProviderFor(project, environment);
1449
+ try {
1450
+ const subjects = await Promise.all(
1451
+ local.map(async ({ resolution }) => ({
1452
+ resolution,
1453
+ meta: await source.readMeta(resolution.ref)
1454
+ }))
1455
+ );
1456
+ return { kind: "read", subjects };
1457
+ } catch (error) {
1458
+ return {
1459
+ kind: "unreachable",
1460
+ finding: {
1461
+ check: "rotation-overdue",
1462
+ severity: "unknown",
1463
+ label: "Rotation",
1464
+ subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
1465
+ detail: errorDetail(error)
1466
+ }
1467
+ };
1468
+ }
1469
+ }
1470
+ function overdueFindings(subjects, environment, now) {
1471
+ const findings = [];
1472
+ for (const { resolution, meta } of subjects) {
1473
+ const { policy, lastRotated } = rotationOf(meta, environment);
1474
+ if (policy === void 0 || lastRotated === null) {
1475
+ continue;
1476
+ }
1477
+ const interval = tryParseDuration(policy);
1478
+ if (interval === void 0) {
1479
+ findings.push({
1480
+ check: "rotation-overdue",
1481
+ severity: "warning",
1482
+ label: "Rotation policy invalid",
1483
+ subject: resolution.parameter,
1484
+ detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
1485
+ });
1486
+ continue;
1487
+ }
1488
+ const last = Date.parse(lastRotated);
1489
+ if (Number.isNaN(last)) {
1490
+ continue;
1491
+ }
1492
+ const overdueBy = now.getTime() - last - interval;
1493
+ if (overdueBy <= 0) {
1494
+ continue;
1495
+ }
1496
+ findings.push({
1497
+ check: "rotation-overdue",
1498
+ severity: "warning",
1499
+ label: "Rotation overdue",
1500
+ subject: resolution.parameter,
1501
+ detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
1502
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
1503
+ });
1504
+ }
1505
+ if (findings.length > 0) {
1506
+ return findings;
1507
+ }
1508
+ return [
1509
+ {
1510
+ check: "rotation-overdue",
1511
+ severity: "pass",
1512
+ label: "Rotation freshness",
1513
+ subject: `no parameter is past its rotation policy for ${environment}`
1514
+ }
1515
+ ];
1516
+ }
1517
+ function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1518
+ const findings = [];
1519
+ for (const { resolution, meta } of subjects) {
1520
+ if (!isStuck(meta, environment, now, stuckThresholdMs)) {
1521
+ continue;
1522
+ }
1523
+ const { rotatingSince } = rotationOf(meta, environment);
1524
+ if (rotatingSince === null) {
1525
+ continue;
1526
+ }
1527
+ const openFor = now.getTime() - Date.parse(rotatingSince);
1528
+ findings.push({
1529
+ check: "rotation-stuck",
1530
+ severity: "warning",
1531
+ label: "Rotation stuck",
1532
+ subject: resolution.parameter,
1533
+ detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
1534
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
1535
+ });
1536
+ }
1537
+ if (findings.length > 0) {
1538
+ return findings;
1539
+ }
1540
+ return [
1541
+ {
1542
+ check: "rotation-stuck",
1543
+ severity: "pass",
1544
+ label: "Rotation progress",
1545
+ subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
1546
+ }
1547
+ ];
1548
+ }
1549
+ function driftKey(file) {
1550
+ return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
1551
+ }
1552
+ function scopeKey(scope) {
1553
+ switch (scope.kind) {
1554
+ case "unscoped":
1555
+ return "unscoped";
1556
+ case "environment":
1557
+ return `environment:${scope.environment}`;
1558
+ case "local":
1559
+ return "local";
1560
+ case "environment-local":
1561
+ return `environment-local:${scope.environment}`;
1562
+ default:
1563
+ return assertNever(scope, "scope");
1564
+ }
1565
+ }
1566
+ function relevantToEnvironment(file, environment) {
1567
+ const scope = file.scope;
1568
+ if (scope.kind === "unscoped") return true;
1569
+ if (scope.kind === "environment") return scope.environment === environment;
1570
+ return false;
1571
+ }
1572
+ async function readRelevant(provider, environment) {
1573
+ const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
1574
+ const stored = await Promise.all(files.map((file) => provider.read(file)));
1575
+ const entries = /* @__PURE__ */ new Map();
1576
+ for (const [index, file] of files.entries()) {
1577
+ const value = stored[index];
1578
+ if (value !== void 0) {
1579
+ entries.set(driftKey(file), { file, stored: value });
1580
+ }
1581
+ }
1582
+ return entries;
1583
+ }
1584
+ async function providerDriftFindings(project, environment, override) {
1585
+ const providerConfig = project.config.providers[environment];
1586
+ if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
1587
+ return [
1588
+ {
1589
+ check: "provider-value-drift",
1590
+ severity: "pass",
1591
+ label: "Provider values",
1592
+ subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
1593
+ }
1594
+ ];
1595
+ }
1596
+ const source = override ?? await sourceProviderFor(project, environment);
1597
+ let local;
1598
+ let remote;
1599
+ try {
1600
+ local = await readRelevant(project.provider, environment);
1601
+ remote = await readRelevant(source, environment);
1602
+ } catch (error) {
1603
+ return [
1604
+ {
1605
+ check: "provider-value-drift",
1606
+ severity: "unknown",
1607
+ label: "Provider values",
1608
+ subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
1609
+ detail: errorDetail(error)
1610
+ }
1611
+ ];
1612
+ }
1613
+ const keys = keySourceFor(project, environment);
1614
+ const findings = [];
1615
+ const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
1616
+ for (const address of addresses) {
1617
+ const here = local.get(address);
1618
+ const there = remote.get(address);
1619
+ if (here !== void 0 && there !== void 0) {
1620
+ const opened = openValue2(here.file, here.stored, keys);
1621
+ if (opened.kind !== "plaintext") {
1622
+ findings.push({
1623
+ check: "provider-value-drift",
1624
+ severity: "unknown",
1625
+ label: "Provider value unreadable",
1626
+ subject: formatValueFile2(here.file),
1627
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
1628
+ });
1629
+ continue;
1630
+ }
1631
+ if (opened.value !== there.stored) {
1632
+ findings.push({
1633
+ check: "provider-value-drift",
1634
+ severity: "failure",
1635
+ label: "Provider value drift",
1636
+ subject: formatValueFile2(here.file),
1637
+ detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
1638
+ });
1639
+ }
1640
+ continue;
1641
+ }
1642
+ const present = here ?? there;
1643
+ if (present === void 0) {
1644
+ continue;
1645
+ }
1646
+ findings.push({
1647
+ check: "provider-value-drift",
1648
+ severity: "warning",
1649
+ label: here !== void 0 ? "Only in the local tree" : "Only in the source",
1650
+ subject: formatValueFile2(present.file),
1651
+ 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`
1652
+ });
1653
+ }
1654
+ if (findings.length > 0) {
1655
+ return findings;
1656
+ }
1657
+ return [
1658
+ {
1659
+ check: "provider-value-drift",
1660
+ severity: "pass",
1661
+ label: "Provider values",
1662
+ subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
1663
+ }
1664
+ ];
1665
+ }
1268
1666
  function renderDoctor(report) {
1269
1667
  const rows = report.findings.map((finding) => ({
1270
1668
  glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
@@ -1307,9 +1705,9 @@ var doctorCommand = defineCommand3({
1307
1705
 
1308
1706
  // src/commands/encrypt.ts
1309
1707
  import {
1310
- formatValueFile as formatValueFile3,
1708
+ formatValueFile as formatValueFile4,
1311
1709
  isSecret as isSecret3,
1312
- openValue as openValue2,
1710
+ openValue as openValue3,
1313
1711
  PenvError as PenvError7,
1314
1712
  parameterId as parameterId4,
1315
1713
  sealValue as sealValue2
@@ -1317,7 +1715,7 @@ import {
1317
1715
  import { defineCommand as defineCommand5 } from "citty";
1318
1716
 
1319
1717
  // src/commands/set.ts
1320
- import { formatValueFile as formatValueFile2, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
1718
+ import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
1321
1719
  import { defineCommand as defineCommand4 } from "citty";
1322
1720
  function scopeFrom(options) {
1323
1721
  if (options.local === true) {
@@ -1352,38 +1750,51 @@ function sealFor(project, file, value, parameter, environment) {
1352
1750
  if (environment === void 0) {
1353
1751
  throw new PenvError6(
1354
1752
  "SECRET_SCOPE_AMBIGUOUS",
1355
- `Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile2(file)} names no environment`,
1753
+ `Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
1356
1754
  "Keys are declared per environment in the `keys` block of penv.config.ts, so penv cannot tell which key should seal a file that every environment reads. Write it at an environment scope \u2014 add `--env <environment>` \u2014 or drop `secret` from the parameter's meta."
1357
1755
  );
1358
1756
  }
1359
1757
  return sealValue(file, value, keySourceFor(project, environment), parameter, environment);
1360
1758
  }
1361
- async function runSet(options) {
1362
- const project = openProject(options.cwd);
1363
- const ref = refFromKey(options.key, project.config);
1364
- const scope = targetScope(project, options, options.key);
1365
- const environment = policyEnvironment(project, options);
1366
- const secret = isSecret2(await project.provider.readMeta(ref), environment);
1759
+ async function sealAwareWrite(options) {
1760
+ const { project, provider, ref, scope, value, environment } = options;
1761
+ const secret = isSecret2(await provider.readMeta(ref), environment);
1367
1762
  const file = {
1368
1763
  namespace: ref.namespace,
1369
1764
  name: ref.name,
1370
1765
  scope,
1371
1766
  encrypted: secret
1372
1767
  };
1373
- const stored = secret ? sealFor(project, file, options.value, parameterId3(ref), environment) : options.value;
1374
- await project.provider.write(file, stored);
1375
- await project.provider.remove({ ...file, encrypted: !secret });
1376
- return { parameter: options.key, location: formatValueFile2(file), encrypted: secret };
1768
+ const stored = secret ? sealFor(project, file, value, parameterId3(ref), environment) : value;
1769
+ await provider.write(file, stored);
1770
+ await provider.remove({ ...file, encrypted: !secret });
1771
+ return { encrypted: secret, location: formatValueFile3(file) };
1772
+ }
1773
+ async function runSet(options) {
1774
+ const project = openProject(options.cwd);
1775
+ assertWritableKey(options.key);
1776
+ const ref = refFromKey(options.key, project.config);
1777
+ const scope = targetScope(project, options, options.key);
1778
+ const environment = policyEnvironment(project, options);
1779
+ const { encrypted, location } = await sealAwareWrite({
1780
+ project,
1781
+ provider: project.provider,
1782
+ ref,
1783
+ scope,
1784
+ value: options.value,
1785
+ environment
1786
+ });
1787
+ return { parameter: options.key, location, encrypted };
1377
1788
  }
1378
- function renderSet(result) {
1789
+ function renderSet(result2) {
1379
1790
  return formatRows([
1380
1791
  {
1381
1792
  glyph: CHECK,
1382
1793
  label: "Wrote",
1383
- subject: `${PENV_DIR}/${result.location}`,
1794
+ subject: `${PENV_DIR}/${result2.location}`,
1384
1795
  // The `.enc` suffix says this already, but only to a reader who knows the
1385
1796
  // grammar. Meta decided it, not the command line, so the command says so.
1386
- ...result.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1797
+ ...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1387
1798
  }
1388
1799
  ]);
1389
1800
  }
@@ -1460,7 +1871,7 @@ async function runEncrypt(options) {
1460
1871
  const already = await readOne(project, sealed);
1461
1872
  throw new PenvError7(
1462
1873
  "PARAMETER_ABSENT",
1463
- already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile3(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile3(sealed)}`,
1874
+ already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile4(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile4(sealed)}`,
1464
1875
  already === void 0 ? `Write it first with \`penv set ${options.key} --env ${environment}\`, which seals it automatically when the parameter's meta declares it a secret.` : "Nothing to do."
1465
1876
  );
1466
1877
  }
@@ -1469,8 +1880,8 @@ async function runEncrypt(options) {
1469
1880
  await project.provider.remove(plain);
1470
1881
  return {
1471
1882
  parameter,
1472
- location: formatValueFile3(sealed),
1473
- removed: formatValueFile3(plain)
1883
+ location: formatValueFile4(sealed),
1884
+ removed: formatValueFile4(plain)
1474
1885
  };
1475
1886
  }
1476
1887
  async function runDecrypt(options) {
@@ -1489,16 +1900,16 @@ async function runDecrypt(options) {
1489
1900
  if (stored === void 0) {
1490
1901
  throw new PenvError7(
1491
1902
  "PARAMETER_ABSENT",
1492
- `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile3(sealed)}`,
1903
+ `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
1493
1904
  `Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
1494
1905
  );
1495
1906
  }
1496
- const opened = openValue2(sealed, stored, keySourceFor(project, environment));
1907
+ const opened = openValue3(sealed, stored, keySourceFor(project, environment));
1497
1908
  if (opened.kind === "failed") {
1498
1909
  throw new UndecryptableAt(
1499
1910
  parameter,
1500
1911
  environment,
1501
- formatValueFile3(sealed),
1912
+ formatValueFile4(sealed),
1502
1913
  opened.failure.detail
1503
1914
  );
1504
1915
  }
@@ -1506,8 +1917,8 @@ async function runDecrypt(options) {
1506
1917
  await project.provider.remove(sealed);
1507
1918
  return {
1508
1919
  parameter,
1509
- location: formatValueFile3(plain),
1510
- removed: formatValueFile3(sealed)
1920
+ location: formatValueFile4(plain),
1921
+ removed: formatValueFile4(sealed)
1511
1922
  };
1512
1923
  }
1513
1924
  var UndecryptableAt = class extends PenvError7 {
@@ -1519,13 +1930,13 @@ var UndecryptableAt = class extends PenvError7 {
1519
1930
  );
1520
1931
  }
1521
1932
  };
1522
- function renderReseal(result, verb) {
1933
+ function renderReseal(result2, verb) {
1523
1934
  return formatRows([
1524
1935
  {
1525
1936
  glyph: CHECK,
1526
1937
  label: verb,
1527
- subject: `${PENV_DIR}/${result.location}`,
1528
- detail: `${PENV_DIR}/${result.removed} removed`
1938
+ subject: `${PENV_DIR}/${result2.location}`,
1939
+ detail: `${PENV_DIR}/${result2.removed} removed`
1529
1940
  }
1530
1941
  ]);
1531
1942
  }
@@ -1548,8 +1959,8 @@ var encryptCommand = defineCommand5({
1548
1959
  args: SCOPE_ARGS,
1549
1960
  run({ args }) {
1550
1961
  return guard(async () => {
1551
- const result = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1552
- write(renderReseal(result, "Encrypted"));
1962
+ const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1963
+ write(renderReseal(result2, "Encrypted"));
1553
1964
  });
1554
1965
  }
1555
1966
  });
@@ -1558,24 +1969,129 @@ var decryptCommand = defineCommand5({
1558
1969
  args: SCOPE_ARGS,
1559
1970
  run({ args }) {
1560
1971
  return guard(async () => {
1561
- const result = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1562
- write(renderReseal(result, "Decrypted"));
1972
+ const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1973
+ write(renderReseal(result2, "Decrypted"));
1974
+ });
1975
+ }
1976
+ });
1977
+
1978
+ // src/commands/fill.ts
1979
+ import { createInterface } from "readline/promises";
1980
+ import { PenvError as PenvError8 } from "@penvhq/core";
1981
+ import { defineCommand as defineCommand6 } from "citty";
1982
+ var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1983
+ async function runFill(options) {
1984
+ const validation = await runValidate({
1985
+ cwd: options.cwd,
1986
+ ...options.environment === void 0 ? {} : { environment: options.environment }
1987
+ });
1988
+ const environment = validation.environment;
1989
+ const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
1990
+ if (blockers.length > 0) {
1991
+ const detail = blockers.map(
1992
+ (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
1993
+ ).join("\n");
1994
+ throw new PenvError8(
1995
+ "FILL_BLOCKED",
1996
+ `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
1997
+ ${detail}`,
1998
+ `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.`
1999
+ );
2000
+ }
2001
+ const written = [];
2002
+ const skipped2 = [];
2003
+ const unreachable = [];
2004
+ for (const drift of validation.drift.declared) {
2005
+ if (drift.ref === void 0) {
2006
+ unreachable.push({ subject: drift.subject, remedy: drift.remedy });
2007
+ continue;
2008
+ }
2009
+ const ref = drift.ref;
2010
+ const key = [...ref.namespace, ref.name].join("/");
2011
+ const value = await options.ask({ parameter: key, environment, secret: false });
2012
+ if (value === void 0 || value === "") {
2013
+ skipped2.push(drift.subject);
2014
+ continue;
2015
+ }
2016
+ const result2 = await runSet({ cwd: options.cwd, key, value, environment });
2017
+ written.push({
2018
+ parameter: drift.subject,
2019
+ location: result2.location,
2020
+ encrypted: result2.encrypted
2021
+ });
2022
+ }
2023
+ return { environment, written, skipped: skipped2, unreachable };
2024
+ }
2025
+ function summaryLine(result2) {
2026
+ const filled = result2.written.length;
2027
+ if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
2028
+ return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2029
+ }
2030
+ const parts = [`${filled} written`];
2031
+ if (result2.skipped.length > 0) {
2032
+ parts.push(`${result2.skipped.length} skipped`);
2033
+ }
2034
+ if (result2.unreachable.length > 0) {
2035
+ parts.push(`${result2.unreachable.length} unreachable`);
2036
+ }
2037
+ return `${parts.join(", ")} for environment ${result2.environment}`;
2038
+ }
2039
+ function renderFill(result2) {
2040
+ const rows = result2.written.map((entry) => ({
2041
+ glyph: CHECK,
2042
+ label: "Wrote",
2043
+ subject: `${PENV_DIR}/${entry.location}`,
2044
+ // Meta decided the seal, not the answer, so the line says so — as `set` does.
2045
+ ...entry.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
2046
+ }));
2047
+ for (const entry of result2.unreachable) {
2048
+ rows.push({ glyph: WARN, label: "Unreachable", subject: entry.subject, detail: entry.remedy });
2049
+ }
2050
+ const lines = formatRows(rows);
2051
+ lines.push(summaryLine(result2));
2052
+ return lines;
2053
+ }
2054
+ var fillCommand = defineCommand6({
2055
+ meta: {
2056
+ name: "fill",
2057
+ description: "Prompt for each declared parameter the tree has no value for"
2058
+ },
2059
+ args: {
2060
+ env: { type: "string", description: "The environment to fill" }
2061
+ },
2062
+ run({ args }) {
2063
+ return guard(async () => {
2064
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2065
+ const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2066
+ try {
2067
+ write(
2068
+ renderFill(
2069
+ await runFill({
2070
+ cwd: process.cwd(),
2071
+ ...args.env === void 0 ? {} : { environment: args.env },
2072
+ ask
2073
+ })
2074
+ )
2075
+ );
2076
+ } finally {
2077
+ rl.close();
2078
+ }
1563
2079
  });
1564
2080
  }
1565
2081
  });
1566
2082
 
1567
2083
  // src/commands/generate.ts
1568
2084
  import { writeFileSync } from "fs";
1569
- import { isAbsolute, relative, resolve as resolve2 } from "path";
2085
+ import { isAbsolute, relative, resolve as resolve3 } from "path";
1570
2086
  import {
1571
2087
  checkNameCollisions as checkNameCollisions3,
1572
2088
  effectiveMeta as effectiveMeta2,
1573
- PenvError as PenvError8,
2089
+ PenvError as PenvError9,
1574
2090
  requireValue as requireValue2,
1575
2091
  serializeDotenv,
1576
2092
  variableName as variableName4
1577
2093
  } from "@penvhq/core";
1578
- import { defineCommand as defineCommand6 } from "citty";
2094
+ import { defineCommand as defineCommand7 } from "citty";
1579
2095
  var DEFAULT_OUTPUT = ".env";
1580
2096
  function entriesFor(project, environment, allowDecrypt) {
1581
2097
  const keys = keySourceFor(project, environment);
@@ -1594,7 +2110,7 @@ function entriesFor(project, environment, allowDecrypt) {
1594
2110
  const winner = resolution.winner;
1595
2111
  if (winner?.file.encrypted === true) {
1596
2112
  if (!allowDecrypt) {
1597
- throw new PenvError8(
2113
+ throw new PenvError9(
1598
2114
  "ENCRYPTED_VALUE_REFUSED",
1599
2115
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
1600
2116
  `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.`
@@ -1625,7 +2141,7 @@ function runGenerate(options) {
1625
2141
  const project = openProject(options.cwd);
1626
2142
  const environment = targetEnvironment(project, options.environment);
1627
2143
  const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
1628
- const file = options.out === void 0 ? resolve2(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve2(options.cwd, options.out);
2144
+ const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
1629
2145
  writeFileSync(file, serializeDotenv(entries), "utf8");
1630
2146
  return { file, environment, entries: entries.length, decrypted };
1631
2147
  }
@@ -1633,26 +2149,26 @@ function displayPath(cwd, file) {
1633
2149
  const rel = relative(cwd, file);
1634
2150
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
1635
2151
  }
1636
- function renderGenerate(result, cwd) {
2152
+ function renderGenerate(result2, cwd) {
1637
2153
  const rows = [
1638
2154
  {
1639
2155
  glyph: CHECK,
1640
2156
  label: "Generated",
1641
- subject: displayPath(cwd, result.file),
1642
- detail: `${result.entries} variables for environment ${result.environment}`
2157
+ subject: displayPath(cwd, result2.file),
2158
+ detail: `${result2.entries} variables for environment ${result2.environment}`
1643
2159
  }
1644
2160
  ];
1645
- if (result.decrypted > 0) {
2161
+ if (result2.decrypted > 0) {
1646
2162
  rows.push({
1647
2163
  glyph: WARN,
1648
2164
  label: "Decrypted",
1649
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
2165
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
1650
2166
  detail: "written as plaintext into the artifact"
1651
2167
  });
1652
2168
  }
1653
2169
  return formatRows(rows);
1654
2170
  }
1655
- var generateCommand = defineCommand6({
2171
+ var generateCommand = defineCommand7({
1656
2172
  meta: { name: "generate", description: "Write a standard .env artifact for deploy targets" },
1657
2173
  args: {
1658
2174
  env: { type: "string", description: "The environment to generate for" },
@@ -1665,20 +2181,20 @@ var generateCommand = defineCommand6({
1665
2181
  run({ args }) {
1666
2182
  return guard(async () => {
1667
2183
  const cwd = process.cwd();
1668
- const result = runGenerate({
2184
+ const result2 = runGenerate({
1669
2185
  cwd,
1670
2186
  ...args.env === void 0 ? {} : { environment: args.env },
1671
2187
  ...args.out === void 0 ? {} : { out: args.out },
1672
2188
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1673
2189
  });
1674
- write(renderGenerate(result, cwd));
2190
+ write(renderGenerate(result2, cwd));
1675
2191
  });
1676
2192
  }
1677
2193
  });
1678
2194
 
1679
2195
  // src/commands/get.ts
1680
- import { PenvError as PenvError9, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
1681
- import { defineCommand as defineCommand7 } from "citty";
2196
+ import { PenvError as PenvError10, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
2197
+ import { defineCommand as defineCommand8 } from "citty";
1682
2198
  async function runGet(options) {
1683
2199
  const project = openProject(options.cwd);
1684
2200
  const environment = targetEnvironment(project, options.environment);
@@ -1687,7 +2203,7 @@ async function runGet(options) {
1687
2203
  const resolution = await resolveParameter(ref, environment, project.provider, keys);
1688
2204
  const value = requireValue3(resolution, environment);
1689
2205
  if (value === void 0) {
1690
- throw new PenvError9(
2206
+ throw new PenvError10(
1691
2207
  "PARAMETER_ABSENT",
1692
2208
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
1693
2209
  `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.`
@@ -1737,7 +2253,7 @@ function renderExplain(explanation) {
1737
2253
  ...columns(rows).map((line) => ` ${line}`)
1738
2254
  ];
1739
2255
  }
1740
- var getCommand = defineCommand7({
2256
+ var getCommand = defineCommand8({
1741
2257
  meta: { name: "get", description: "Read a parameter" },
1742
2258
  args: {
1743
2259
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -1762,17 +2278,17 @@ var getCommand = defineCommand7({
1762
2278
 
1763
2279
  // src/commands/import.ts
1764
2280
  import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1765
- import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve4 } from "path";
2281
+ import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
1766
2282
  import {
1767
2283
  accessPath as accessPath4,
1768
- assertNever,
2284
+ assertNever as assertNever2,
1769
2285
  checkNameCollisions as checkNameCollisions4,
1770
2286
  FilenameGrammarError,
1771
2287
  findConfigFile,
1772
2288
  isReservedToken as isReservedToken3,
1773
2289
  loadConfigFrom as loadConfigFrom2,
1774
2290
  lookupEnvironment,
1775
- PenvError as PenvError11,
2291
+ PenvError as PenvError12,
1776
2292
  parseDotenv,
1777
2293
  ReservedTokenError as ReservedTokenError3,
1778
2294
  refFromVariable,
@@ -1781,7 +2297,7 @@ import {
1781
2297
  UnknownEnvironmentError,
1782
2298
  variableName as variableName5
1783
2299
  } from "@penvhq/core";
1784
- import { defineCommand as defineCommand9 } from "citty";
2300
+ import { defineCommand as defineCommand10 } from "citty";
1785
2301
 
1786
2302
  // src/detect.ts
1787
2303
  import { existsSync, readFileSync } from "fs";
@@ -1886,19 +2402,19 @@ function hasImportsBlock(cwd) {
1886
2402
 
1887
2403
  // src/commands/init.ts
1888
2404
  import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1889
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
1890
- import { createInterface } from "readline/promises";
2405
+ import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
2406
+ import { createInterface as createInterface2 } from "readline/promises";
1891
2407
  import {
1892
2408
  DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
1893
2409
  isLegalEnvironmentName,
1894
2410
  loadConfigFrom,
1895
- PenvError as PenvError10,
2411
+ PenvError as PenvError11,
1896
2412
  RESERVED_TOKENS,
1897
2413
  schemaFileOf as schemaFileOf2,
1898
2414
  schemaInsideTree,
1899
2415
  validateSchemaFile
1900
2416
  } from "@penvhq/core";
1901
- import { defineCommand as defineCommand8 } from "citty";
2417
+ import { defineCommand as defineCommand9 } from "citty";
1902
2418
  var CONFIG_FILE = "penv.config.ts";
1903
2419
  var TSCONFIG_FILE = "tsconfig.json";
1904
2420
  var GITIGNORE_FILE = ".gitignore";
@@ -1936,7 +2452,7 @@ function suggestEnvironments(root) {
1936
2452
  return [...found].sort();
1937
2453
  }
1938
2454
  function emptyFlag(flag) {
1939
- return new PenvError10(
2455
+ return new PenvError11(
1940
2456
  "INIT_FLAG_EMPTY",
1941
2457
  `\`--${flag}\` was given without a value`,
1942
2458
  flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${DEFAULT_SCHEMA_FILE2}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
@@ -1999,7 +2515,7 @@ function planInit(root, flags = {}) {
1999
2515
  throw emptyFlag("alias");
2000
2516
  }
2001
2517
  if (!ALIAS_NAME.test(alias)) {
2002
- throw new PenvError10(
2518
+ throw new PenvError11(
2003
2519
  "INIT_ALIAS_INVALID",
2004
2520
  `\`${alias}\` is not an alias penv can write`,
2005
2521
  `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.`
@@ -2286,7 +2802,7 @@ ${objectIndent}${source.slice(close)}`;
2286
2802
  ${entryIndent}${member},${source.slice(open + 1)}`;
2287
2803
  }
2288
2804
  function shapeError(what, target, alias) {
2289
- return new PenvError10(
2805
+ return new PenvError11(
2290
2806
  "TSCONFIG_SHAPE",
2291
2807
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
2292
2808
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
@@ -2356,7 +2872,7 @@ function renderTsconfig(target, alias) {
2356
2872
  `;
2357
2873
  }
2358
2874
  function ensurePenvDir(root) {
2359
- const dir = resolve3(root, PENV_DIR2);
2875
+ const dir = resolve4(root, PENV_DIR2);
2360
2876
  if (existsSync2(dir)) {
2361
2877
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
2362
2878
  }
@@ -2373,7 +2889,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2373
2889
  note: "(yours \u2014 penv never regenerates it)"
2374
2890
  };
2375
2891
  }
2376
- mkdirSync(dirname2(file), { recursive: true });
2892
+ mkdirSync(dirname3(file), { recursive: true });
2377
2893
  writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
2378
2894
  return {
2379
2895
  target: "schema",
@@ -2461,26 +2977,26 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2461
2977
  ];
2462
2978
  }
2463
2979
  function runInit(options) {
2464
- const root = resolve3(options.cwd);
2980
+ const root = resolve4(options.cwd);
2465
2981
  const decisions = options.decisions ?? planInit(root).decisions;
2466
2982
  return { root, decisions, steps: scaffold(root, [], false, decisions) };
2467
2983
  }
2468
- function renderInit(result) {
2469
- const steps = result.steps.map((step) => {
2984
+ function renderInit(result2) {
2985
+ const steps = result2.steps.map((step) => {
2470
2986
  const glyph = step.action === "conflicted" ? WARN : CHECK;
2471
2987
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
2472
2988
  });
2473
2989
  return [
2474
2990
  ...formatSteps(steps),
2475
2991
  "",
2476
- `Done. Declare your parameters in ${result.decisions.schemaFile}, then \`penv set <key>\`.`,
2477
- ...result.decisions.environments.length === 0 ? [
2992
+ `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
2993
+ ...result2.decisions.environments.length === 0 ? [
2478
2994
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2479
2995
  ] : []
2480
2996
  ];
2481
2997
  }
2482
2998
  async function askOnTty(plan2) {
2483
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2999
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2484
3000
  try {
2485
3001
  return await promptForDecisions(plan2, {
2486
3002
  ask: (question) => rl.question(question),
@@ -2491,7 +3007,7 @@ async function askOnTty(plan2) {
2491
3007
  rl.close();
2492
3008
  }
2493
3009
  }
2494
- var initCommand = defineCommand8({
3010
+ var initCommand = defineCommand9({
2495
3011
  meta: { name: "init", description: "Initialize a project (.penv/, env.ts, config, @env alias)" },
2496
3012
  args: {
2497
3013
  yes: {
@@ -2513,7 +3029,7 @@ var initCommand = defineCommand8({
2513
3029
  },
2514
3030
  run({ args }) {
2515
3031
  return guard(async () => {
2516
- const root = resolve3(process.cwd());
3032
+ const root = resolve4(process.cwd());
2517
3033
  const environments = environmentsFromFlag(args.env);
2518
3034
  const plan2 = planInit(root, {
2519
3035
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -2565,7 +3081,7 @@ function assertImportable(ref, variable) {
2565
3081
  if (!ref.name.includes(".")) {
2566
3082
  return;
2567
3083
  }
2568
- throw new PenvError11(
3084
+ throw new PenvError12(
2569
3085
  "IMPORT_UNPARSEABLE_NAME",
2570
3086
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
2571
3087
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
@@ -2584,7 +3100,7 @@ function assertRoundTrips(ref, variable, config) {
2584
3100
  return;
2585
3101
  }
2586
3102
  const generated = variableName5(ref, config);
2587
- throw new PenvError11(
3103
+ throw new PenvError12(
2588
3104
  "IMPORT_LOSSY_NAME",
2589
3105
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
2590
3106
  `\`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.`
@@ -2647,7 +3163,7 @@ function environmentOf(scope) {
2647
3163
  case "local":
2648
3164
  return void 0;
2649
3165
  default:
2650
- return assertNever(scope, "scope");
3166
+ return assertNever2(scope, "scope");
2651
3167
  }
2652
3168
  }
2653
3169
  function scopeWithEnvironment(scope, environment) {
@@ -2660,7 +3176,7 @@ function scopeWithEnvironment(scope, environment) {
2660
3176
  case "environment-local":
2661
3177
  return scope;
2662
3178
  default:
2663
- return assertNever(scope, "scope");
3179
+ return assertNever2(scope, "scope");
2664
3180
  }
2665
3181
  }
2666
3182
  function explicitEnvironment(options, source, config) {
@@ -2668,7 +3184,7 @@ function explicitEnvironment(options, source, config) {
2668
3184
  if (value.length > 0) {
2669
3185
  return value;
2670
3186
  }
2671
- throw new PenvError11(
3187
+ throw new PenvError12(
2672
3188
  "IMPORT_ENV_FLAG_EMPTY",
2673
3189
  `\`--env\` for the import of ${source} names no environment`,
2674
3190
  `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.`
@@ -2678,7 +3194,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
2678
3194
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
2679
3195
  return;
2680
3196
  }
2681
- throw new PenvError11(
3197
+ throw new PenvError12(
2682
3198
  "IMPORT_ENV_CONFLICT",
2683
3199
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
2684
3200
  `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.`
@@ -2719,10 +3235,10 @@ function configInEffect(cwd, environment) {
2719
3235
  return { config: openProject(cwd).config, decisions };
2720
3236
  }
2721
3237
  function importDotenv(options) {
2722
- const cwd = resolve4(options.cwd);
2723
- const file = isAbsolute2(options.file) ? options.file : resolve4(cwd, options.file);
3238
+ const cwd = resolve5(options.cwd);
3239
+ const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
2724
3240
  if (!existsSync3(file)) {
2725
- throw new PenvError11(
3241
+ throw new PenvError12(
2726
3242
  "IMPORT_FILE_MISSING",
2727
3243
  `There is no file at ${file} to import`,
2728
3244
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
@@ -2803,19 +3319,19 @@ function skippedValidationStep(environments) {
2803
3319
  note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
2804
3320
  };
2805
3321
  }
2806
- function renderImport(result, validation) {
2807
- const steps = [{ glyph: CHECK, text: `Found ${result.variables} variables` }];
2808
- if (result.orphanComments > 0) {
2809
- const plural = result.orphanComments === 1 ? "comment" : "comments";
3322
+ function renderImport(result2, validation) {
3323
+ const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
3324
+ if (result2.orphanComments > 0) {
3325
+ const plural = result2.orphanComments === 1 ? "comment" : "comments";
2810
3326
  steps.push({
2811
3327
  glyph: WARN,
2812
- text: `Dropped ${result.orphanComments} orphan ${plural}`,
3328
+ text: `Dropped ${result2.orphanComments} orphan ${plural}`,
2813
3329
  note: "attached to no variable, so nothing to describe"
2814
3330
  });
2815
3331
  }
2816
- for (const step of result.steps) {
3332
+ for (const step of result2.steps) {
2817
3333
  if (step.target === "schema" && step.action === "kept") {
2818
- steps.push(keptSchemaStep(step, result.variables));
3334
+ steps.push(keptSchemaStep(step, result2.variables));
2819
3335
  continue;
2820
3336
  }
2821
3337
  const glyph = step.action === "conflicted" ? WARN : CHECK;
@@ -2823,10 +3339,10 @@ function renderImport(result, validation) {
2823
3339
  step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
2824
3340
  );
2825
3341
  }
2826
- steps.push({ glyph: CHECK, text: `Created ${displayPath2(result.root, result.backup)}` });
3342
+ steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
2827
3343
  const lines = formatSteps(steps);
2828
3344
  if (validation === void 0) {
2829
- lines.push(...formatSteps([skippedValidationStep(result.environments)]));
3345
+ lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
2830
3346
  } else if (validation.ok) {
2831
3347
  lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
2832
3348
  } else {
@@ -2835,7 +3351,7 @@ function renderImport(result, validation) {
2835
3351
  lines.push("", "Done. .penv/ is now your source of truth.");
2836
3352
  return lines;
2837
3353
  }
2838
- var importCommand = defineCommand9({
3354
+ var importCommand = defineCommand10({
2839
3355
  meta: {
2840
3356
  name: "import",
2841
3357
  description: "Import an existing dotenv file; it becomes the source of truth"
@@ -2867,15 +3383,15 @@ var importCommand = defineCommand9({
2867
3383
 
2868
3384
  // src/commands/key.ts
2869
3385
  import { randomBytes } from "crypto";
2870
- import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError12 } from "@penvhq/core";
2871
- import { defineCommand as defineCommand10 } from "citty";
3386
+ import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError13 } from "@penvhq/core";
3387
+ import { defineCommand as defineCommand11 } from "citty";
2872
3388
 
2873
3389
  // src/keychain.ts
2874
- import { createRequire } from "module";
3390
+ import { createRequire as createRequire2 } from "module";
2875
3391
  var cached;
2876
3392
  function entryConstructor() {
2877
3393
  if (cached === void 0) {
2878
- const require2 = createRequire(import.meta.url);
3394
+ const require2 = createRequire2(import.meta.url);
2879
3395
  cached = require2("@napi-rs/keyring").Entry;
2880
3396
  }
2881
3397
  return cached;
@@ -2900,7 +3416,7 @@ function runKeyCreate(options) {
2900
3416
  const environment = targetEnvironment(project, options.environment);
2901
3417
  const declared = project.config.keys?.[environment];
2902
3418
  if (declared === void 0) {
2903
- throw new PenvError12(
3419
+ throw new PenvError13(
2904
3420
  "KEY_SOURCE_UNDECLARED",
2905
3421
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
2906
3422
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
@@ -2914,14 +3430,14 @@ function runKeyCreate(options) {
2914
3430
  try {
2915
3431
  existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
2916
3432
  } catch (cause) {
2917
- throw new PenvError12(
3433
+ throw new PenvError13(
2918
3434
  "KEYCHAIN_UNAVAILABLE",
2919
3435
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
2920
3436
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
2921
3437
  );
2922
3438
  }
2923
3439
  if (existing !== null) {
2924
- throw new PenvError12(
3440
+ throw new PenvError13(
2925
3441
  "KEY_EXISTS",
2926
3442
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
2927
3443
  "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."
@@ -2939,29 +3455,29 @@ function runKeyCreate(options) {
2939
3455
  key
2940
3456
  };
2941
3457
  }
2942
- function renderKeyCreate(result) {
2943
- if (result.source === "keychain") {
3458
+ function renderKeyCreate(result2) {
3459
+ if (result2.source === "keychain") {
2944
3460
  return [
2945
- `A new key for environment ${result.environment}, stored in your OS keychain as \`${result.id}\`.`,
3461
+ `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
2946
3462
  "",
2947
3463
  "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
2948
3464
  "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
2949
3465
  ];
2950
3466
  }
2951
3467
  return [
2952
- `A new key for environment ${result.environment}. penv did not store it.`,
3468
+ `A new key for environment ${result2.environment}. penv did not store it.`,
2953
3469
  "",
2954
- ` ${result.variable}=${result.key}`,
3470
+ ` ${result2.variable}=${result2.key}`,
2955
3471
  "",
2956
3472
  "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
2957
3473
  "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
2958
3474
  "without it, and penv keeps no copy to fall back on."
2959
3475
  ];
2960
3476
  }
2961
- var keyCommand = defineCommand10({
3477
+ var keyCommand = defineCommand11({
2962
3478
  meta: { name: "key", description: "Work with encryption keys" },
2963
3479
  subCommands: {
2964
- create: defineCommand10({
3480
+ create: defineCommand11({
2965
3481
  meta: { name: "create", description: "Generate a key for an environment" },
2966
3482
  args: {
2967
3483
  env: { type: "string", description: "The environment the key is for" },
@@ -2988,8 +3504,8 @@ var keyCommand = defineCommand10({
2988
3504
  });
2989
3505
 
2990
3506
  // src/commands/list.ts
2991
- import { assertNever as assertNever2, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
2992
- import { defineCommand as defineCommand11 } from "citty";
3507
+ import { assertNever as assertNever3, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
3508
+ import { defineCommand as defineCommand12 } from "citty";
2993
3509
  function scopeLabel2(scope) {
2994
3510
  switch (scope.kind) {
2995
3511
  case "environment":
@@ -3001,7 +3517,7 @@ function scopeLabel2(scope) {
3001
3517
  case "unscoped":
3002
3518
  return "default";
3003
3519
  default:
3004
- return assertNever2(scope, "scope");
3520
+ return assertNever3(scope, "scope");
3005
3521
  }
3006
3522
  }
3007
3523
  async function runList(options) {
@@ -3023,13 +3539,13 @@ async function runList(options) {
3023
3539
  }
3024
3540
  return { environment, parameters };
3025
3541
  }
3026
- function renderList(result) {
3027
- if (result.parameters.length === 0) {
3028
- return [`No parameters in ${PENV_DIR}/ for environment ${result.environment}.`];
3542
+ function renderList(result2) {
3543
+ if (result2.parameters.length === 0) {
3544
+ return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3029
3545
  }
3030
- return columns(result.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3546
+ return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3031
3547
  }
3032
- var listCommand = defineCommand11({
3548
+ var listCommand = defineCommand12({
3033
3549
  meta: { name: "list", description: "List parameters" },
3034
3550
  args: {
3035
3551
  env: { type: "string", description: "The environment to resolve against" },
@@ -3037,11 +3553,11 @@ var listCommand = defineCommand11({
3037
3553
  },
3038
3554
  run({ args }) {
3039
3555
  return guard(async () => {
3040
- const result = await runList({
3556
+ const result2 = await runList({
3041
3557
  cwd: process.cwd(),
3042
3558
  ...args.env === void 0 ? {} : { environment: args.env }
3043
3559
  });
3044
- write(args.json === true ? [JSON.stringify(result, null, 2)] : renderList(result));
3560
+ write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
3045
3561
  });
3046
3562
  }
3047
3563
  });
@@ -3050,13 +3566,13 @@ var listCommand = defineCommand11({
3050
3566
  import {
3051
3567
  accessPath as accessPath5,
3052
3568
  formatMetaFile,
3053
- formatValueFile as formatValueFile4,
3054
- openValue as openValue3,
3055
- PenvError as PenvError13,
3569
+ formatValueFile as formatValueFile5,
3570
+ openValue as openValue4,
3571
+ PenvError as PenvError14,
3056
3572
  parameterId as parameterId5,
3057
3573
  sealValue as sealValue3
3058
3574
  } from "@penvhq/core";
3059
- import { defineCommand as defineCommand12 } from "citty";
3575
+ import { defineCommand as defineCommand13 } from "citty";
3060
3576
  function environmentOf2(file) {
3061
3577
  const scope = file.scope;
3062
3578
  return scope.kind === "environment" || scope.kind === "environment-local" ? scope.environment : void 0;
@@ -3071,18 +3587,18 @@ async function planFile(project, source, target, parameter) {
3071
3587
  }
3072
3588
  const environment = environmentOf2(source);
3073
3589
  if (environment === void 0) {
3074
- throw new PenvError13(
3590
+ throw new PenvError14(
3075
3591
  "SECRET_SCOPE_AMBIGUOUS",
3076
- `${PENV_DIR}/${formatValueFile4(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3592
+ `${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3077
3593
  "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."
3078
3594
  );
3079
3595
  }
3080
3596
  const keys = keySourceFor(project, environment);
3081
- const opened = openValue3(source, stored, keys);
3597
+ const opened = openValue4(source, stored, keys);
3082
3598
  if (opened.kind === "failed") {
3083
- throw new PenvError13(
3599
+ throw new PenvError14(
3084
3600
  "VALUE_UNDECRYPTABLE",
3085
- `${PENV_DIR}/${formatValueFile4(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3601
+ `${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3086
3602
  "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."
3087
3603
  );
3088
3604
  }
@@ -3100,9 +3616,10 @@ function filesOf(all, ref) {
3100
3616
  async function runMove(options) {
3101
3617
  const project = openProject(options.cwd);
3102
3618
  const from = refFromKey(options.from, project.config);
3619
+ assertWritableKey(options.to);
3103
3620
  const to = refFromKey(options.to, project.config);
3104
3621
  if (parameterId5(from) === parameterId5(to)) {
3105
- throw new PenvError13(
3622
+ throw new PenvError14(
3106
3623
  "PARAMETER_UNCHANGED",
3107
3624
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
3108
3625
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -3112,7 +3629,7 @@ async function runMove(options) {
3112
3629
  const sources = filesOf(all, from);
3113
3630
  const meta = await project.provider.readMeta(from);
3114
3631
  if (sources.length === 0 && meta === void 0) {
3115
- throw new PenvError13(
3632
+ throw new PenvError14(
3116
3633
  "PARAMETER_ABSENT",
3117
3634
  `Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
3118
3635
  `\`penv list\` shows every parameter penv holds.`
@@ -3120,7 +3637,7 @@ async function runMove(options) {
3120
3637
  }
3121
3638
  const occupied2 = filesOf(all, to);
3122
3639
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
3123
- throw new PenvError13(
3640
+ throw new PenvError14(
3124
3641
  "PARAMETER_EXISTS",
3125
3642
  `Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
3126
3643
  `Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
@@ -3150,8 +3667,8 @@ async function runMove(options) {
3150
3667
  from: parameterId5(from),
3151
3668
  to: parameterId5(to),
3152
3669
  files: planned.map((file) => ({
3153
- from: formatValueFile4(file.source),
3154
- to: formatValueFile4(file.target),
3670
+ from: formatValueFile5(file.source),
3671
+ to: formatValueFile5(file.target),
3155
3672
  resealed: file.resealed
3156
3673
  })),
3157
3674
  // The meta's path, not the parameter's dotted id: `redis.password` is what
@@ -3161,25 +3678,25 @@ async function runMove(options) {
3161
3678
  schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
3162
3679
  };
3163
3680
  }
3164
- function renderMove(result) {
3165
- const rows = result.files.map((file) => ({
3681
+ function renderMove(result2) {
3682
+ const rows = result2.files.map((file) => ({
3166
3683
  glyph: CHECK,
3167
3684
  label: "Moved",
3168
3685
  subject: `${PENV_DIR}/${file.to}`,
3169
3686
  ...file.resealed ? { detail: "re-sealed for its new address" } : {}
3170
3687
  }));
3171
- if (result.meta !== void 0) {
3172
- rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result.meta}` });
3688
+ if (result2.meta !== void 0) {
3689
+ rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
3173
3690
  }
3174
3691
  const lines = formatRows(rows);
3175
3692
  lines.push(
3176
3693
  "",
3177
- ` .penv/env.ts still declares \`${result.schema.was}\`. Rename it to \`${result.schema.now}\`,`,
3694
+ ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3178
3695
  " or `penv validate` will report the value as unused and the declaration as unset."
3179
3696
  );
3180
3697
  return lines;
3181
3698
  }
3182
- var mvCommand = defineCommand12({
3699
+ var mvCommand = defineCommand13({
3183
3700
  meta: { name: "mv", description: "Rename a parameter, every scope and its meta at once" },
3184
3701
  args: {
3185
3702
  from: {
@@ -3200,9 +3717,86 @@ var mvCommand = defineCommand12({
3200
3717
  }
3201
3718
  });
3202
3719
 
3720
+ // src/commands/pull.ts
3721
+ import { defineCommand as defineCommand14 } from "citty";
3722
+ async function runPull(options) {
3723
+ const project = openProject(options.cwd);
3724
+ const environment = targetEnvironment(project, options.environment);
3725
+ const source = await sourceProviderFor(project, environment);
3726
+ if (source.type === LOCAL_TREE_TYPE) {
3727
+ return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
3728
+ }
3729
+ const tree = localTree(project);
3730
+ const files = await source.list();
3731
+ let values = 0;
3732
+ for (const file of files) {
3733
+ const value = await source.read(file);
3734
+ if (value === void 0) {
3735
+ continue;
3736
+ }
3737
+ tree.writeSync(file, value);
3738
+ values += 1;
3739
+ }
3740
+ const refs = refsFrom(files);
3741
+ let meta = 0;
3742
+ for (const ref of refs) {
3743
+ const block = await source.readMeta(ref);
3744
+ if (block === void 0) {
3745
+ continue;
3746
+ }
3747
+ tree.writeMetaSync(ref, block);
3748
+ meta += 1;
3749
+ }
3750
+ return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
3751
+ }
3752
+ function renderPull(result2) {
3753
+ if (result2.localSource) {
3754
+ return formatRows([
3755
+ {
3756
+ glyph: CHECK,
3757
+ label: "Nothing to pull",
3758
+ subject: `environment ${result2.environment} has no separate source of truth`,
3759
+ detail: "its values live in the local .penv tree already"
3760
+ }
3761
+ ]);
3762
+ }
3763
+ return formatRows([
3764
+ {
3765
+ glyph: CHECK,
3766
+ label: "Pulled",
3767
+ subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
3768
+ detail: `from the ${result2.source} provider for environment ${result2.environment}`
3769
+ },
3770
+ {
3771
+ glyph: CHECK,
3772
+ label: "Parameters",
3773
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
3774
+ detail: "written into the local .penv tree"
3775
+ }
3776
+ ]);
3777
+ }
3778
+ var pullCommand = defineCommand14({
3779
+ meta: {
3780
+ name: "pull",
3781
+ description: "Materialise the local .penv tree from an environment's source-of-truth provider"
3782
+ },
3783
+ args: {
3784
+ env: { type: "string", description: "The environment to pull" }
3785
+ },
3786
+ run({ args }) {
3787
+ return guard(async () => {
3788
+ const result2 = await runPull({
3789
+ cwd: process.cwd(),
3790
+ ...args.env === void 0 ? {} : { environment: args.env }
3791
+ });
3792
+ write(renderPull(result2));
3793
+ });
3794
+ }
3795
+ });
3796
+
3203
3797
  // src/commands/remove.ts
3204
- import { formatValueFile as formatValueFile5 } from "@penvhq/core";
3205
- import { defineCommand as defineCommand13 } from "citty";
3798
+ import { formatValueFile as formatValueFile6 } from "@penvhq/core";
3799
+ import { defineCommand as defineCommand15 } from "citty";
3206
3800
  async function runRemove(options) {
3207
3801
  const project = openProject(options.cwd);
3208
3802
  const ref = refFromKey(options.key);
@@ -3219,17 +3813,17 @@ async function runRemove(options) {
3219
3813
  continue;
3220
3814
  }
3221
3815
  await project.provider.remove(file);
3222
- removed.push(formatValueFile5(file));
3816
+ removed.push(formatValueFile6(file));
3223
3817
  }
3224
3818
  return {
3225
3819
  parameter: options.key,
3226
3820
  removed,
3227
- considered: files.map((file) => formatValueFile5(file))
3821
+ considered: files.map((file) => formatValueFile6(file))
3228
3822
  };
3229
3823
  }
3230
- function renderRemove(result) {
3231
- if (result.removed.length === 0) {
3232
- const first = result.considered[0] ?? result.parameter;
3824
+ function renderRemove(result2) {
3825
+ if (result2.removed.length === 0) {
3826
+ const first = result2.considered[0] ?? result2.parameter;
3233
3827
  return formatRows([
3234
3828
  {
3235
3829
  glyph: WARN,
@@ -3239,14 +3833,14 @@ function renderRemove(result) {
3239
3833
  }
3240
3834
  ]);
3241
3835
  }
3242
- const rows = result.removed.map((location) => ({
3836
+ const rows = result2.removed.map((location) => ({
3243
3837
  glyph: CHECK,
3244
3838
  label: "Removed",
3245
3839
  subject: `${PENV_DIR}/${location}`
3246
3840
  }));
3247
3841
  return formatRows(rows);
3248
3842
  }
3249
- var removeCommand = defineCommand13({
3843
+ var removeCommand = defineCommand15({
3250
3844
  meta: { name: "remove", description: "Delete a parameter" },
3251
3845
  args: {
3252
3846
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -3272,11 +3866,197 @@ var removeCommand = defineCommand13({
3272
3866
  }
3273
3867
  });
3274
3868
 
3869
+ // src/commands/rotate.ts
3870
+ import {
3871
+ beginRotation,
3872
+ completeRotation,
3873
+ PenvError as PenvError15,
3874
+ retainsPrevious,
3875
+ rotationOf as rotationOf2
3876
+ } from "@penvhq/core";
3877
+ import { defineCommand as defineCommand16 } from "citty";
3878
+ function rotatingFile(ref, environment) {
3879
+ return {
3880
+ namespace: ref.namespace,
3881
+ name: ref.name,
3882
+ scope: { kind: "environment", environment },
3883
+ encrypted: false
3884
+ };
3885
+ }
3886
+ async function writeRotatedValue(project, provider, ref, environment, value) {
3887
+ if (provider.type === LOCAL_TREE_TYPE) {
3888
+ await sealAwareWrite({
3889
+ project,
3890
+ provider,
3891
+ ref,
3892
+ scope: { kind: "environment", environment },
3893
+ value,
3894
+ environment
3895
+ });
3896
+ return;
3897
+ }
3898
+ await provider.write(rotatingFile(ref, environment), value);
3899
+ }
3900
+ function requireNewValue(value, phase, key) {
3901
+ if (value === void 0) {
3902
+ throw new PenvError15(
3903
+ "ROTATION_NO_VALUE",
3904
+ `A ${phase} rotation of ${key} writes a new value, and none was given`,
3905
+ "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
3906
+ );
3907
+ }
3908
+ return value;
3909
+ }
3910
+ function requireRetaining(provider, environment) {
3911
+ if (!retainsPrevious(provider)) {
3912
+ throw new PenvError15(
3913
+ "ROTATION_NOT_RETAINING",
3914
+ `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`,
3915
+ "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."
3916
+ );
3917
+ }
3918
+ return provider;
3919
+ }
3920
+ async function runRotate(options) {
3921
+ const project = openProject(options.cwd);
3922
+ const environment = targetEnvironment(project, options.environment);
3923
+ const ref = refFromKey(options.key, project.config);
3924
+ const provider = await sourceProviderFor(project, environment);
3925
+ const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
3926
+ const before = await provider.readMeta(ref);
3927
+ const { mechanism } = rotationOf2(before, environment);
3928
+ if (mechanism === void 0) {
3929
+ throw new PenvError15(
3930
+ "ROTATION_NO_MECHANISM",
3931
+ `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
3932
+ '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.'
3933
+ );
3934
+ }
3935
+ const begin = options.begin === true;
3936
+ const complete = options.complete === true;
3937
+ if (mechanism === "atomic-cutover") {
3938
+ if (begin || complete) {
3939
+ throw new PenvError15(
3940
+ "ROTATION_MECHANISM_MISMATCH",
3941
+ `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
3942
+ "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."
3943
+ );
3944
+ }
3945
+ const value = requireNewValue(options.value, "cutover", options.key);
3946
+ await writeRotatedValue(project, provider, ref, environment, value);
3947
+ const after2 = completeRotation(before, environment, nowIso);
3948
+ await provider.writeMeta(ref, after2);
3949
+ return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
3950
+ }
3951
+ const retaining = requireRetaining(provider, environment);
3952
+ if (begin === complete) {
3953
+ throw new PenvError15(
3954
+ "ROTATION_PHASE_REQUIRED",
3955
+ `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
3956
+ "`--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."
3957
+ );
3958
+ }
3959
+ if (begin) {
3960
+ const value = requireNewValue(options.value, "begin", options.key);
3961
+ await writeRotatedValue(project, retaining, ref, environment, value);
3962
+ const after2 = beginRotation(before, environment, nowIso);
3963
+ await retaining.writeMeta(ref, after2);
3964
+ return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
3965
+ }
3966
+ const after = completeRotation(before, environment, nowIso);
3967
+ await retaining.writeMeta(ref, after);
3968
+ return result(ref, environment, mechanism, "complete", retaining.type, false, after);
3969
+ }
3970
+ function result(ref, environment, mechanism, phase, source, wroteValue, after) {
3971
+ const { state, rotatingSince, lastRotated } = rotationOf2(after, environment);
3972
+ return {
3973
+ parameter: [...ref.namespace, ref.name].join("/"),
3974
+ environment,
3975
+ mechanism,
3976
+ phase,
3977
+ source,
3978
+ wroteValue,
3979
+ state,
3980
+ rotatingSince,
3981
+ lastRotated
3982
+ };
3983
+ }
3984
+ function renderRotate(result2) {
3985
+ if (result2.phase === "begin") {
3986
+ return formatRows([
3987
+ {
3988
+ glyph: CHECK,
3989
+ label: "Rotating",
3990
+ subject: result2.parameter,
3991
+ detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
3992
+ },
3993
+ {
3994
+ glyph: CHECK,
3995
+ label: "Previous",
3996
+ subject: "still readable",
3997
+ detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
3998
+ }
3999
+ ]);
4000
+ }
4001
+ if (result2.phase === "complete") {
4002
+ return formatRows([
4003
+ {
4004
+ glyph: CHECK,
4005
+ label: "Rotated",
4006
+ subject: result2.parameter,
4007
+ detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
4008
+ }
4009
+ ]);
4010
+ }
4011
+ return formatRows([
4012
+ {
4013
+ glyph: CHECK,
4014
+ label: "Rotated",
4015
+ subject: result2.parameter,
4016
+ detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
4017
+ }
4018
+ ]);
4019
+ }
4020
+ var rotateCommand = defineCommand16({
4021
+ meta: {
4022
+ name: "rotate",
4023
+ description: "Rotate a parameter's value by the mechanism its meta declares"
4024
+ },
4025
+ args: {
4026
+ key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
4027
+ value: {
4028
+ type: "positional",
4029
+ required: false,
4030
+ description: "The new value; read from stdin if omitted. Not needed with --complete"
4031
+ },
4032
+ env: { type: "string", description: "The environment to rotate in" },
4033
+ begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
4034
+ complete: { type: "boolean", description: "Close a dual-valid grace window" }
4035
+ },
4036
+ run({ args }) {
4037
+ return guard(async () => {
4038
+ const value = args.complete === true ? void 0 : args.value ?? await readStdin();
4039
+ write(
4040
+ renderRotate(
4041
+ await runRotate({
4042
+ cwd: process.cwd(),
4043
+ key: args.key,
4044
+ ...value === void 0 ? {} : { value },
4045
+ ...args.env === void 0 ? {} : { environment: args.env },
4046
+ ...args.begin === void 0 ? {} : { begin: args.begin },
4047
+ ...args.complete === void 0 ? {} : { complete: args.complete }
4048
+ })
4049
+ )
4050
+ );
4051
+ });
4052
+ }
4053
+ });
4054
+
3275
4055
  // src/commands/watch.ts
3276
4056
  import { existsSync as existsSync4, watch } from "fs";
3277
- import { basename as basename2, dirname as dirname3, resolve as resolve5 } from "path";
4057
+ import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
3278
4058
  import { schemaFileOf as schemaFileOf4, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
3279
- import { defineCommand as defineCommand14 } from "citty";
4059
+ import { defineCommand as defineCommand17 } from "citty";
3280
4060
  var DEBOUNCE_MS = 100;
3281
4061
  function runWatch(options) {
3282
4062
  const project = openProject(options.cwd);
@@ -3297,12 +4077,12 @@ function runWatch(options) {
3297
4077
  }
3298
4078
  running = true;
3299
4079
  try {
3300
- const result = await runValidate({
4080
+ const result2 = await runValidate({
3301
4081
  cwd: options.cwd,
3302
4082
  ...options.environment === void 0 ? {} : { environment: options.environment }
3303
4083
  });
3304
4084
  if (!closed) {
3305
- options.onResult?.(result);
4085
+ options.onResult?.(result2);
3306
4086
  }
3307
4087
  } catch (error) {
3308
4088
  if (!closed) {
@@ -3333,7 +4113,7 @@ function runWatch(options) {
3333
4113
  watcher.close();
3334
4114
  }
3335
4115
  function armRecovery(target, recursive, only) {
3336
- const parent = dirname3(target);
4116
+ const parent = dirname4(target);
3337
4117
  const name = basename2(target);
3338
4118
  let recovery;
3339
4119
  try {
@@ -3407,10 +4187,10 @@ function runWatch(options) {
3407
4187
  watchers.add(watcher);
3408
4188
  }
3409
4189
  addWatcher(project.penvDir, true);
3410
- addWatcher(dirname3(project.configFile), false, configFile);
4190
+ addWatcher(dirname4(project.configFile), false, configFile);
3411
4191
  if (schemaInsideTree2(project.config) === void 0) {
3412
- const schemaFile = resolve5(project.root, schemaFileOf4(project.config));
3413
- addWatcher(dirname3(schemaFile), false, basename2(schemaFile));
4192
+ const schemaFile = resolve6(project.root, schemaFileOf4(project.config));
4193
+ addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
3414
4194
  }
3415
4195
  void validate();
3416
4196
  return {
@@ -3426,8 +4206,8 @@ function runWatch(options) {
3426
4206
  }
3427
4207
  };
3428
4208
  }
3429
- function renderWatch(result) {
3430
- return ["", ...renderValidate(result), ...renderDrift(result.drift, result.environment)];
4209
+ function renderWatch(result2) {
4210
+ return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
3431
4211
  }
3432
4212
  function renderDrift(drift, environment) {
3433
4213
  const rows = [
@@ -3453,7 +4233,7 @@ function renderDrift(drift, environment) {
3453
4233
  }
3454
4234
  return lines;
3455
4235
  }
3456
- var watchCommand = defineCommand14({
4236
+ var watchCommand = defineCommand17({
3457
4237
  meta: {
3458
4238
  name: "watch",
3459
4239
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -3466,8 +4246,8 @@ var watchCommand = defineCommand14({
3466
4246
  const handle = runWatch({
3467
4247
  cwd: process.cwd(),
3468
4248
  ...args.env === void 0 ? {} : { environment: args.env },
3469
- onResult: (result) => {
3470
- write(renderWatch(result));
4249
+ onResult: (result2) => {
4250
+ write(renderWatch(result2));
3471
4251
  },
3472
4252
  // A failing cycle is reported and watching continues. `reportError`
3473
4253
  // marks the process failed, which `validate` wants and a loop does not:
@@ -3481,10 +4261,10 @@ var watchCommand = defineCommand14({
3481
4261
  }
3482
4262
  });
3483
4263
  write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
3484
- await new Promise((resolve6) => {
4264
+ await new Promise((resolve7) => {
3485
4265
  process.once("SIGINT", () => {
3486
4266
  handle.close();
3487
- resolve6();
4267
+ resolve7();
3488
4268
  });
3489
4269
  });
3490
4270
  });
@@ -3492,7 +4272,7 @@ var watchCommand = defineCommand14({
3492
4272
  });
3493
4273
 
3494
4274
  // src/index.ts
3495
- var main = defineCommand15({
4275
+ var main = defineCommand18({
3496
4276
  meta: {
3497
4277
  name: "penv",
3498
4278
  description: "Configuration that shares a data model with your production secret manager"
@@ -3503,8 +4283,11 @@ var main = defineCommand15({
3503
4283
  generate: generateCommand,
3504
4284
  get: getCommand,
3505
4285
  set: setCommand,
4286
+ fill: fillCommand,
3506
4287
  mv: mvCommand,
4288
+ pull: pullCommand,
3507
4289
  push: pushCommand,
4290
+ rotate: rotateCommand,
3508
4291
  remove: removeCommand,
3509
4292
  list: listCommand,
3510
4293
  encrypt: encryptCommand,
@@ -3526,21 +4309,27 @@ export {
3526
4309
  insertEnvAlias,
3527
4310
  main,
3528
4311
  renderDoctor,
4312
+ renderFill,
3529
4313
  renderMove,
4314
+ renderPull,
3530
4315
  renderPush,
4316
+ renderRotate,
3531
4317
  renderWatch,
3532
4318
  runDecrypt,
3533
4319
  runDoctor,
3534
4320
  runEncrypt,
3535
4321
  runExplain,
4322
+ runFill,
3536
4323
  runGenerate,
3537
4324
  runGet,
3538
4325
  runInit,
3539
4326
  runList,
3540
4327
  runMain,
3541
4328
  runMove,
4329
+ runPull,
3542
4330
  runPush,
3543
4331
  runRemove,
4332
+ runRotate,
3544
4333
  runSet,
3545
4334
  runValidate,
3546
4335
  runWatch