@penvhq/cli 0.1.0 → 0.2.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
@@ -27,7 +27,9 @@ __export(index_exports, {
27
27
  main: () => main,
28
28
  renderDoctor: () => renderDoctor,
29
29
  renderMove: () => renderMove,
30
+ renderPull: () => renderPull,
30
31
  renderPush: () => renderPush,
32
+ renderRotate: () => renderRotate,
31
33
  renderWatch: () => renderWatch,
32
34
  runDecrypt: () => runDecrypt,
33
35
  runDoctor: () => runDoctor,
@@ -39,15 +41,17 @@ __export(index_exports, {
39
41
  runList: () => runList,
40
42
  runMain: () => runMain,
41
43
  runMove: () => runMove,
44
+ runPull: () => runPull,
42
45
  runPush: () => runPush,
43
46
  runRemove: () => runRemove,
47
+ runRotate: () => runRotate,
44
48
  runSet: () => runSet,
45
49
  runValidate: () => runValidate,
46
50
  runWatch: () => runWatch
47
51
  });
48
52
  module.exports = __toCommonJS(index_exports);
49
- var import_core20 = require("@penvhq/core");
50
- var import_citty15 = require("citty");
53
+ var import_core21 = require("@penvhq/core");
54
+ var import_citty17 = require("citty");
51
55
 
52
56
  // src/commands/doctor.ts
53
57
  var import_core7 = require("@penvhq/core");
@@ -55,17 +59,35 @@ var import_sink_github2 = require("@penvhq/sink-github");
55
59
  var import_citty3 = require("citty");
56
60
 
57
61
  // src/project.ts
58
- var import_node_path = require("path");
62
+ var import_node_path2 = require("path");
59
63
  var import_core2 = require("@penvhq/core");
60
64
  var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
61
65
 
62
66
  // src/registry.ts
67
+ var import_node_module = require("module");
68
+ var import_node_path = require("path");
69
+ var import_node_url = require("url");
63
70
  var import_core = require("@penvhq/core");
64
71
  var import_provider_filesystem = require("@penvhq/provider-filesystem");
72
+ var import_provider_mock = require("@penvhq/provider-mock");
73
+ var import_provider_vault = require("@penvhq/provider-vault");
74
+ var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
65
75
  var LOCAL_TREE_TYPE = "filesystem";
66
76
  var REGISTRY = /* @__PURE__ */ new Map([
67
- [LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })]
77
+ [LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })],
78
+ ["vault", ({ providerConfig }) => (0, import_provider_vault.createVaultProvider)({ path: providerConfig?.path ?? "penv" })],
79
+ ["mock", ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })]
68
80
  ]);
81
+ var CONTRACT_METHODS = [
82
+ "read",
83
+ "write",
84
+ "list",
85
+ "remove",
86
+ "readMeta",
87
+ "writeMeta",
88
+ "removeMeta"
89
+ ];
90
+ var pluginModuleCache = /* @__PURE__ */ new Map();
69
91
  function isProviderRegistered(type) {
70
92
  return REGISTRY.has(type);
71
93
  }
@@ -76,20 +98,94 @@ function createProvider(type, context) {
76
98
  }
77
99
  return factory(context);
78
100
  }
79
- function assertProvidersRegistered(config) {
101
+ async function createSourceProvider(type, context) {
102
+ if (REGISTRY.has(type)) {
103
+ return createProvider(type, context);
104
+ }
105
+ return loadPluginProvider(type, context);
106
+ }
107
+ async function loadPluginProvider(type, context) {
108
+ const fromDir = resolutionBase(context);
109
+ const specifier = pluginSpecifier(context.providerConfig, type);
110
+ const resolved = resolvePlugin(specifier, fromDir);
111
+ if (resolved === void 0) {
112
+ throw unknownProvider(type, context.environment, specifier);
113
+ }
114
+ let mod;
115
+ try {
116
+ mod = await importPlugin(resolved);
117
+ } catch {
118
+ throw new import_core.PenvError(
119
+ "PROVIDER_PLUGIN_LOAD",
120
+ `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
121
+ "It resolved but threw while importing. Check it builds and its dependencies are installed."
122
+ );
123
+ }
124
+ const factory = mod[PLUGIN_FACTORY_EXPORT];
125
+ if (typeof factory !== "function") {
126
+ throw new import_core.PenvError(
127
+ "PROVIDER_PLUGIN_INVALID",
128
+ `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
129
+ `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
130
+ );
131
+ }
132
+ const provider = await factory(context);
133
+ assertSatisfiesContract(provider, specifier);
134
+ return provider;
135
+ }
136
+ function assertProvidersRegistered(config, projectRoot) {
80
137
  for (const [environment, provider] of Object.entries(config.providers)) {
81
- if (!isProviderRegistered(provider.type)) {
82
- throw unknownProvider(provider.type, environment);
138
+ if (isProviderRegistered(provider.type)) {
139
+ continue;
83
140
  }
141
+ const specifier = pluginSpecifier(provider, provider.type);
142
+ if (resolvePlugin(specifier, projectRoot) === void 0) {
143
+ throw unknownProvider(provider.type, environment, specifier);
144
+ }
145
+ }
146
+ }
147
+ function pluginSpecifier(providerConfig, type) {
148
+ return providerConfig?.module ?? `@penvhq/provider-${type}`;
149
+ }
150
+ function resolutionBase(context) {
151
+ return (0, import_node_path.dirname)(context.root);
152
+ }
153
+ function resolvePlugin(specifier, fromDir) {
154
+ try {
155
+ const require2 = (0, import_node_module.createRequire)((0, import_node_path.resolve)(fromDir, "noop.js"));
156
+ return require2.resolve(specifier);
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ }
161
+ function importPlugin(resolvedPath) {
162
+ const cached2 = pluginModuleCache.get(resolvedPath);
163
+ if (cached2 !== void 0) {
164
+ return cached2;
84
165
  }
166
+ const loading = import((0, import_node_url.pathToFileURL)(resolvedPath).href);
167
+ pluginModuleCache.set(resolvedPath, loading);
168
+ return loading;
85
169
  }
86
- function unknownProvider(type, environment) {
170
+ function assertSatisfiesContract(provider, specifier) {
171
+ for (const method of CONTRACT_METHODS) {
172
+ if (typeof provider[method] !== "function") {
173
+ throw new import_core.PenvError(
174
+ "PROVIDER_PLUGIN_INVALID",
175
+ `The provider from \`${specifier}\` is missing \`${method}()\``,
176
+ "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
177
+ );
178
+ }
179
+ }
180
+ }
181
+ function unknownProvider(type, environment, specifier) {
87
182
  const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
88
183
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
184
+ 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
185
  return new import_core.PenvError(
90
186
  "UNKNOWN_PROVIDER",
91
187
  `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}\`.`
188
+ remedy
93
189
  );
94
190
  }
95
191
 
@@ -97,9 +193,9 @@ function unknownProvider(type, environment) {
97
193
  var PENV_DIR = ".penv";
98
194
  function openProject(cwd) {
99
195
  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);
196
+ const root = (0, import_node_path2.dirname)(file);
197
+ const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
198
+ assertProvidersRegistered(config, root);
103
199
  return {
104
200
  root,
105
201
  configFile: file,
@@ -118,6 +214,18 @@ function localTree(project) {
118
214
  }
119
215
  return project.provider;
120
216
  }
217
+ async function sourceProviderFor(project, environment) {
218
+ const providerConfig = project.config.providers[environment];
219
+ if (providerConfig === void 0) {
220
+ return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
221
+ }
222
+ return createSourceProvider(providerConfig.type, {
223
+ root: project.penvDir,
224
+ config: project.config,
225
+ providerConfig,
226
+ environment
227
+ });
228
+ }
121
229
  function targetEnvironment(project, explicit) {
122
230
  return (0, import_core2.resolveEnvironment)(project.config, explicit);
123
231
  }
@@ -493,36 +601,36 @@ async function runPush(options) {
493
601
  decrypted: outbound.filter((item) => item.encrypted).length
494
602
  };
495
603
  }
496
- function renderPush(result) {
497
- if (result.pushed === 0) {
604
+ function renderPush(result2) {
605
+ if (result2.pushed === 0) {
498
606
  return formatRows([
499
607
  {
500
608
  glyph: CHECK,
501
609
  label: "Nothing to push",
502
- subject: `no values resolve for environment ${result.environment}`
610
+ subject: `no values resolve for environment ${result2.environment}`
503
611
  }
504
612
  ]);
505
613
  }
506
- const target = `GitHub Actions for environment ${result.environment}${result.repo === void 0 ? "" : ` (${result.repo})`}`;
614
+ const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
507
615
  const rows = [
508
616
  {
509
617
  glyph: CHECK,
510
618
  label: "Pushed",
511
- subject: `${result.pushed} ${result.pushed === 1 ? "secret" : "secrets"}`,
619
+ subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
512
620
  detail: `to ${target}`
513
621
  },
514
622
  {
515
623
  glyph: CHECK,
516
624
  label: "Scopes",
517
- subject: `${result.environmentSecrets} environment, ${result.repositorySecrets} repository`,
625
+ subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
518
626
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
519
627
  }
520
628
  ];
521
- if (result.decrypted > 0) {
629
+ if (result2.decrypted > 0) {
522
630
  rows.push({
523
631
  glyph: WARN,
524
632
  label: "Decrypted",
525
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
633
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
526
634
  detail: "sent as plaintext for GitHub to re-seal under its own key"
527
635
  });
528
636
  }
@@ -542,19 +650,19 @@ var pushCommand = (0, import_citty.defineCommand)({
542
650
  },
543
651
  run({ args }) {
544
652
  return guard(async () => {
545
- const result = await runPush({
653
+ const result2 = await runPush({
546
654
  cwd: process.cwd(),
547
655
  ...args.env === void 0 ? {} : { environment: args.env },
548
656
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
549
657
  });
550
- write(renderPush(result));
658
+ write(renderPush(result2));
551
659
  });
552
660
  }
553
661
  });
554
662
 
555
663
  // src/commands/validate.ts
556
- var import_node_path2 = require("path");
557
- var import_node_url = require("url");
664
+ var import_node_path3 = require("path");
665
+ var import_node_url2 = require("url");
558
666
  var import_core6 = require("@penvhq/core");
559
667
  var import_citty2 = require("citty");
560
668
  var import_jiti = require("jiti");
@@ -623,16 +731,16 @@ function validationIssuesOf(cause, schemaPath) {
623
731
  }
624
732
  var schemaLoads = Promise.resolve();
625
733
  function exclusively(work) {
626
- const result = schemaLoads.then(work, work);
627
- schemaLoads = result.then(
734
+ const result2 = schemaLoads.then(work, work);
735
+ schemaLoads = result2.then(
628
736
  () => void 0,
629
737
  () => void 0
630
738
  );
631
- return result;
739
+ return result2;
632
740
  }
633
741
  function loadSchema(project, environment) {
634
742
  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;
743
+ const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
636
744
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
637
745
  }
638
746
  async function loadSchemaExclusively(file, schemaPath, environment) {
@@ -734,9 +842,9 @@ async function runValidate(options) {
734
842
  }
735
843
  }
736
844
  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) {
845
+ const result2 = schema.safeParse(object);
846
+ if (!result2.success) {
847
+ for (const problem of result2.error.issues) {
740
848
  const path = problem.path.join(".");
741
849
  if (undecryptable.has(path)) {
742
850
  continue;
@@ -752,24 +860,24 @@ async function runValidate(options) {
752
860
  }
753
861
  return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
754
862
  }
755
- function renderValidate(result) {
756
- if (result.ok) {
863
+ function renderValidate(result2) {
864
+ if (result2.ok) {
757
865
  return formatRows([
758
866
  {
759
867
  glyph: CHECK,
760
868
  label: "Schema valid",
761
- subject: `${result.parameters} parameters for environment ${result.environment}`
869
+ subject: `${result2.parameters} parameters for environment ${result2.environment}`
762
870
  }
763
871
  ]);
764
872
  }
765
- const rows = result.issues.map((issue) => ({
873
+ const rows = result2.issues.map((issue) => ({
766
874
  glyph: WARN,
767
875
  label: LABELS[issue.kind],
768
876
  subject: issue.subject,
769
877
  detail: issue.message
770
878
  }));
771
879
  const lines = formatRows(rows);
772
- const remedies = [...new Set(result.issues.map((issue) => issue.remedy))];
880
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
773
881
  for (const remedy of remedies) {
774
882
  if (remedy !== void 0) {
775
883
  lines.push(` ${remedy}`);
@@ -787,12 +895,12 @@ var validateCommand = (0, import_citty2.defineCommand)({
787
895
  },
788
896
  run({ args }) {
789
897
  return guard(async () => {
790
- const result = await runValidate({
898
+ const result2 = await runValidate({
791
899
  cwd: process.cwd(),
792
900
  ...args.env === void 0 ? {} : { environment: args.env }
793
901
  });
794
- write(renderValidate(result));
795
- if (!result.ok) {
902
+ write(renderValidate(result2));
903
+ if (!result2.ok) {
796
904
  process.exitCode = 1;
797
905
  }
798
906
  });
@@ -800,6 +908,7 @@ var validateCommand = (0, import_citty2.defineCommand)({
800
908
  });
801
909
 
802
910
  // src/commands/doctor.ts
911
+ var STUCK_THRESHOLD_MS = 864e5;
803
912
  function skipped(check, label) {
804
913
  return {
805
914
  check,
@@ -813,6 +922,8 @@ async function runDoctor(options) {
813
922
  const project = openProject(options.cwd);
814
923
  const environment = targetEnvironment(project, options.environment);
815
924
  const findings = [];
925
+ const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
926
+ const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
816
927
  const { schema, issues } = await loadSchema(project, environment);
817
928
  if (schema !== void 0) {
818
929
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -861,6 +972,14 @@ async function runDoctor(options) {
861
972
  findings.push(...plaintextSecretFindings(subjects, environment));
862
973
  findings.push(...publicSecretFindings(subjects, environment, project.config));
863
974
  findings.push(...encryptionFindings(subjects, environment));
975
+ const rotation = await rotationSubjects(project, environment, subjects, options.source);
976
+ if (rotation.kind === "unreachable") {
977
+ findings.push(rotation.finding);
978
+ } else {
979
+ findings.push(...overdueFindings(rotation.subjects, environment, now));
980
+ findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
981
+ }
982
+ findings.push(...await providerDriftFindings(project, environment, options.source));
864
983
  findings.push(...await sinkFindings(project, environment, options.sink));
865
984
  findings.push({
866
985
  check: "provider",
@@ -1278,6 +1397,246 @@ async function sinkFindings(project, environment, override) {
1278
1397
  });
1279
1398
  return findings;
1280
1399
  }
1400
+ function humanizeMs(ms) {
1401
+ const abs = Math.max(0, ms);
1402
+ const day = 864e5;
1403
+ const hour = 36e5;
1404
+ const minute = 6e4;
1405
+ const round = (value, unit) => {
1406
+ const n = Math.round(value);
1407
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
1408
+ };
1409
+ if (abs >= day) return round(abs / day, "day");
1410
+ if (abs >= hour) return round(abs / hour, "hour");
1411
+ if (abs >= minute) return round(abs / minute, "minute");
1412
+ return round(abs / 1e3, "second");
1413
+ }
1414
+ function refPathOf(ref) {
1415
+ return [...ref.namespace, ref.name].join("/");
1416
+ }
1417
+ async function rotationSubjects(project, environment, local, override) {
1418
+ const providerConfig = project.config.providers[environment];
1419
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
1420
+ return { kind: "read", subjects: local };
1421
+ }
1422
+ const source = override ?? await sourceProviderFor(project, environment);
1423
+ try {
1424
+ const subjects = await Promise.all(
1425
+ local.map(async ({ resolution }) => ({
1426
+ resolution,
1427
+ meta: await source.readMeta(resolution.ref)
1428
+ }))
1429
+ );
1430
+ return { kind: "read", subjects };
1431
+ } catch (error) {
1432
+ return {
1433
+ kind: "unreachable",
1434
+ finding: {
1435
+ check: "rotation-overdue",
1436
+ severity: "unknown",
1437
+ label: "Rotation",
1438
+ subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
1439
+ detail: errorDetail(error)
1440
+ }
1441
+ };
1442
+ }
1443
+ }
1444
+ function overdueFindings(subjects, environment, now) {
1445
+ const findings = [];
1446
+ for (const { resolution, meta } of subjects) {
1447
+ const { policy, lastRotated } = (0, import_core7.rotationOf)(meta, environment);
1448
+ if (policy === void 0 || lastRotated === null) {
1449
+ continue;
1450
+ }
1451
+ const interval = (0, import_core7.tryParseDuration)(policy);
1452
+ if (interval === void 0) {
1453
+ findings.push({
1454
+ check: "rotation-overdue",
1455
+ severity: "warning",
1456
+ label: "Rotation policy invalid",
1457
+ subject: resolution.parameter,
1458
+ detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
1459
+ });
1460
+ continue;
1461
+ }
1462
+ const last = Date.parse(lastRotated);
1463
+ if (Number.isNaN(last)) {
1464
+ continue;
1465
+ }
1466
+ const overdueBy = now.getTime() - last - interval;
1467
+ if (overdueBy <= 0) {
1468
+ continue;
1469
+ }
1470
+ findings.push({
1471
+ check: "rotation-overdue",
1472
+ severity: "warning",
1473
+ label: "Rotation overdue",
1474
+ subject: resolution.parameter,
1475
+ detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
1476
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
1477
+ });
1478
+ }
1479
+ if (findings.length > 0) {
1480
+ return findings;
1481
+ }
1482
+ return [
1483
+ {
1484
+ check: "rotation-overdue",
1485
+ severity: "pass",
1486
+ label: "Rotation freshness",
1487
+ subject: `no parameter is past its rotation policy for ${environment}`
1488
+ }
1489
+ ];
1490
+ }
1491
+ function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1492
+ const findings = [];
1493
+ for (const { resolution, meta } of subjects) {
1494
+ if (!(0, import_core7.isStuck)(meta, environment, now, stuckThresholdMs)) {
1495
+ continue;
1496
+ }
1497
+ const { rotatingSince } = (0, import_core7.rotationOf)(meta, environment);
1498
+ if (rotatingSince === null) {
1499
+ continue;
1500
+ }
1501
+ const openFor = now.getTime() - Date.parse(rotatingSince);
1502
+ findings.push({
1503
+ check: "rotation-stuck",
1504
+ severity: "warning",
1505
+ label: "Rotation stuck",
1506
+ subject: resolution.parameter,
1507
+ detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
1508
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
1509
+ });
1510
+ }
1511
+ if (findings.length > 0) {
1512
+ return findings;
1513
+ }
1514
+ return [
1515
+ {
1516
+ check: "rotation-stuck",
1517
+ severity: "pass",
1518
+ label: "Rotation progress",
1519
+ subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
1520
+ }
1521
+ ];
1522
+ }
1523
+ function driftKey(file) {
1524
+ return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
1525
+ }
1526
+ function scopeKey(scope) {
1527
+ switch (scope.kind) {
1528
+ case "unscoped":
1529
+ return "unscoped";
1530
+ case "environment":
1531
+ return `environment:${scope.environment}`;
1532
+ case "local":
1533
+ return "local";
1534
+ case "environment-local":
1535
+ return `environment-local:${scope.environment}`;
1536
+ default:
1537
+ return (0, import_core7.assertNever)(scope, "scope");
1538
+ }
1539
+ }
1540
+ function relevantToEnvironment(file, environment) {
1541
+ const scope = file.scope;
1542
+ if (scope.kind === "unscoped") return true;
1543
+ if (scope.kind === "environment") return scope.environment === environment;
1544
+ return false;
1545
+ }
1546
+ async function readRelevant(provider, environment) {
1547
+ const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
1548
+ const stored = await Promise.all(files.map((file) => provider.read(file)));
1549
+ const entries = /* @__PURE__ */ new Map();
1550
+ for (const [index, file] of files.entries()) {
1551
+ const value = stored[index];
1552
+ if (value !== void 0) {
1553
+ entries.set(driftKey(file), { file, stored: value });
1554
+ }
1555
+ }
1556
+ return entries;
1557
+ }
1558
+ async function providerDriftFindings(project, environment, override) {
1559
+ const providerConfig = project.config.providers[environment];
1560
+ if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
1561
+ return [
1562
+ {
1563
+ check: "provider-value-drift",
1564
+ severity: "pass",
1565
+ label: "Provider values",
1566
+ subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
1567
+ }
1568
+ ];
1569
+ }
1570
+ const source = override ?? await sourceProviderFor(project, environment);
1571
+ let local;
1572
+ let remote;
1573
+ try {
1574
+ local = await readRelevant(project.provider, environment);
1575
+ remote = await readRelevant(source, environment);
1576
+ } catch (error) {
1577
+ return [
1578
+ {
1579
+ check: "provider-value-drift",
1580
+ severity: "unknown",
1581
+ label: "Provider values",
1582
+ subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
1583
+ detail: errorDetail(error)
1584
+ }
1585
+ ];
1586
+ }
1587
+ const keys = keySourceFor(project, environment);
1588
+ const findings = [];
1589
+ const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
1590
+ for (const address of addresses) {
1591
+ const here = local.get(address);
1592
+ const there = remote.get(address);
1593
+ if (here !== void 0 && there !== void 0) {
1594
+ const opened = (0, import_core7.openValue)(here.file, here.stored, keys);
1595
+ if (opened.kind !== "plaintext") {
1596
+ findings.push({
1597
+ check: "provider-value-drift",
1598
+ severity: "unknown",
1599
+ label: "Provider value unreadable",
1600
+ subject: (0, import_core7.formatValueFile)(here.file),
1601
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
1602
+ });
1603
+ continue;
1604
+ }
1605
+ if (opened.value !== there.stored) {
1606
+ findings.push({
1607
+ check: "provider-value-drift",
1608
+ severity: "failure",
1609
+ label: "Provider value drift",
1610
+ subject: (0, import_core7.formatValueFile)(here.file),
1611
+ detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
1612
+ });
1613
+ }
1614
+ continue;
1615
+ }
1616
+ const present = here ?? there;
1617
+ if (present === void 0) {
1618
+ continue;
1619
+ }
1620
+ findings.push({
1621
+ check: "provider-value-drift",
1622
+ severity: "warning",
1623
+ label: here !== void 0 ? "Only in the local tree" : "Only in the source",
1624
+ subject: (0, import_core7.formatValueFile)(present.file),
1625
+ 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`
1626
+ });
1627
+ }
1628
+ if (findings.length > 0) {
1629
+ return findings;
1630
+ }
1631
+ return [
1632
+ {
1633
+ check: "provider-value-drift",
1634
+ severity: "pass",
1635
+ label: "Provider values",
1636
+ subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
1637
+ }
1638
+ ];
1639
+ }
1281
1640
  function renderDoctor(report) {
1282
1641
  const rows = report.findings.map((finding) => ({
1283
1642
  glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
@@ -1364,32 +1723,44 @@ function sealFor(project, file, value, parameter, environment) {
1364
1723
  }
1365
1724
  return (0, import_core8.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
1366
1725
  }
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);
1726
+ async function sealAwareWrite(options) {
1727
+ const { project, provider, ref, scope, value, environment } = options;
1728
+ const secret = (0, import_core8.isSecret)(await provider.readMeta(ref), environment);
1373
1729
  const file = {
1374
1730
  namespace: ref.namespace,
1375
1731
  name: ref.name,
1376
1732
  scope,
1377
1733
  encrypted: secret
1378
1734
  };
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 };
1735
+ const stored = secret ? sealFor(project, file, value, (0, import_core8.parameterId)(ref), environment) : value;
1736
+ await provider.write(file, stored);
1737
+ await provider.remove({ ...file, encrypted: !secret });
1738
+ return { encrypted: secret, location: (0, import_core8.formatValueFile)(file) };
1383
1739
  }
1384
- function renderSet(result) {
1740
+ async function runSet(options) {
1741
+ const project = openProject(options.cwd);
1742
+ const ref = refFromKey(options.key, project.config);
1743
+ const scope = targetScope(project, options, options.key);
1744
+ const environment = policyEnvironment(project, options);
1745
+ const { encrypted, location } = await sealAwareWrite({
1746
+ project,
1747
+ provider: project.provider,
1748
+ ref,
1749
+ scope,
1750
+ value: options.value,
1751
+ environment
1752
+ });
1753
+ return { parameter: options.key, location, encrypted };
1754
+ }
1755
+ function renderSet(result2) {
1385
1756
  return formatRows([
1386
1757
  {
1387
1758
  glyph: CHECK,
1388
1759
  label: "Wrote",
1389
- subject: `${PENV_DIR}/${result.location}`,
1760
+ subject: `${PENV_DIR}/${result2.location}`,
1390
1761
  // The `.enc` suffix says this already, but only to a reader who knows the
1391
1762
  // grammar. Meta decided it, not the command line, so the command says so.
1392
- ...result.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1763
+ ...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1393
1764
  }
1394
1765
  ]);
1395
1766
  }
@@ -1525,13 +1896,13 @@ var UndecryptableAt = class extends import_core9.PenvError {
1525
1896
  );
1526
1897
  }
1527
1898
  };
1528
- function renderReseal(result, verb) {
1899
+ function renderReseal(result2, verb) {
1529
1900
  return formatRows([
1530
1901
  {
1531
1902
  glyph: CHECK,
1532
1903
  label: verb,
1533
- subject: `${PENV_DIR}/${result.location}`,
1534
- detail: `${PENV_DIR}/${result.removed} removed`
1904
+ subject: `${PENV_DIR}/${result2.location}`,
1905
+ detail: `${PENV_DIR}/${result2.removed} removed`
1535
1906
  }
1536
1907
  ]);
1537
1908
  }
@@ -1554,8 +1925,8 @@ var encryptCommand = (0, import_citty5.defineCommand)({
1554
1925
  args: SCOPE_ARGS,
1555
1926
  run({ args }) {
1556
1927
  return guard(async () => {
1557
- const result = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1558
- write(renderReseal(result, "Encrypted"));
1928
+ const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1929
+ write(renderReseal(result2, "Encrypted"));
1559
1930
  });
1560
1931
  }
1561
1932
  });
@@ -1564,15 +1935,15 @@ var decryptCommand = (0, import_citty5.defineCommand)({
1564
1935
  args: SCOPE_ARGS,
1565
1936
  run({ args }) {
1566
1937
  return guard(async () => {
1567
- const result = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1568
- write(renderReseal(result, "Decrypted"));
1938
+ const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1939
+ write(renderReseal(result2, "Decrypted"));
1569
1940
  });
1570
1941
  }
1571
1942
  });
1572
1943
 
1573
1944
  // src/commands/generate.ts
1574
1945
  var import_node_fs = require("fs");
1575
- var import_node_path3 = require("path");
1946
+ var import_node_path4 = require("path");
1576
1947
  var import_core10 = require("@penvhq/core");
1577
1948
  var import_citty6 = require("citty");
1578
1949
  var DEFAULT_OUTPUT = ".env";
@@ -1624,28 +1995,28 @@ function runGenerate(options) {
1624
1995
  const project = openProject(options.cwd);
1625
1996
  const environment = targetEnvironment(project, options.environment);
1626
1997
  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);
1998
+ 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);
1628
1999
  (0, import_node_fs.writeFileSync)(file, (0, import_core10.serializeDotenv)(entries), "utf8");
1629
2000
  return { file, environment, entries: entries.length, decrypted };
1630
2001
  }
1631
2002
  function displayPath(cwd, file) {
1632
- const rel = (0, import_node_path3.relative)(cwd, file);
2003
+ const rel = (0, import_node_path4.relative)(cwd, file);
1633
2004
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
1634
2005
  }
1635
- function renderGenerate(result, cwd) {
2006
+ function renderGenerate(result2, cwd) {
1636
2007
  const rows = [
1637
2008
  {
1638
2009
  glyph: CHECK,
1639
2010
  label: "Generated",
1640
- subject: displayPath(cwd, result.file),
1641
- detail: `${result.entries} variables for environment ${result.environment}`
2011
+ subject: displayPath(cwd, result2.file),
2012
+ detail: `${result2.entries} variables for environment ${result2.environment}`
1642
2013
  }
1643
2014
  ];
1644
- if (result.decrypted > 0) {
2015
+ if (result2.decrypted > 0) {
1645
2016
  rows.push({
1646
2017
  glyph: WARN,
1647
2018
  label: "Decrypted",
1648
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
2019
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
1649
2020
  detail: "written as plaintext into the artifact"
1650
2021
  });
1651
2022
  }
@@ -1664,13 +2035,13 @@ var generateCommand = (0, import_citty6.defineCommand)({
1664
2035
  run({ args }) {
1665
2036
  return guard(async () => {
1666
2037
  const cwd = process.cwd();
1667
- const result = runGenerate({
2038
+ const result2 = runGenerate({
1668
2039
  cwd,
1669
2040
  ...args.env === void 0 ? {} : { environment: args.env },
1670
2041
  ...args.out === void 0 ? {} : { out: args.out },
1671
2042
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1672
2043
  });
1673
- write(renderGenerate(result, cwd));
2044
+ write(renderGenerate(result2, cwd));
1674
2045
  });
1675
2046
  }
1676
2047
  });
@@ -1761,13 +2132,13 @@ var getCommand = (0, import_citty7.defineCommand)({
1761
2132
 
1762
2133
  // src/commands/import.ts
1763
2134
  var import_node_fs4 = require("fs");
1764
- var import_node_path6 = require("path");
2135
+ var import_node_path7 = require("path");
1765
2136
  var import_core14 = require("@penvhq/core");
1766
2137
  var import_citty9 = require("citty");
1767
2138
 
1768
2139
  // src/detect.ts
1769
2140
  var import_node_fs2 = require("fs");
1770
- var import_node_path4 = require("path");
2141
+ var import_node_path5 = require("path");
1771
2142
  var import_core12 = require("@penvhq/core");
1772
2143
  var SIGNATURES = [
1773
2144
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
@@ -1793,11 +2164,11 @@ function exportsSchema(file) {
1793
2164
  );
1794
2165
  }
1795
2166
  function occupied(cwd, relative3) {
1796
- const file = (0, import_node_path4.join)(cwd, ...relative3.split("/"));
2167
+ const file = (0, import_node_path5.join)(cwd, ...relative3.split("/"));
1797
2168
  return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
1798
2169
  }
1799
2170
  function schemaFileFor(cwd) {
1800
- const dir = (0, import_node_fs2.existsSync)((0, import_node_path4.join)(cwd, "src")) ? "src/" : "";
2171
+ const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
1801
2172
  const preferred = `${dir}env.ts`;
1802
2173
  if (!occupied(cwd, preferred)) {
1803
2174
  return { file: preferred };
@@ -1825,7 +2196,7 @@ function dependenciesOf(cwd) {
1825
2196
  return names;
1826
2197
  }
1827
2198
  function manifestOf(cwd) {
1828
- const file = (0, import_node_path4.join)(cwd, "package.json");
2199
+ const file = (0, import_node_path5.join)(cwd, "package.json");
1829
2200
  if (!(0, import_node_fs2.existsSync)(file)) {
1830
2201
  return void 0;
1831
2202
  }
@@ -1868,7 +2239,7 @@ function hasImportsBlock(cwd) {
1868
2239
 
1869
2240
  // src/commands/init.ts
1870
2241
  var import_node_fs3 = require("fs");
1871
- var import_node_path5 = require("path");
2242
+ var import_node_path6 = require("path");
1872
2243
  var import_promises = require("readline/promises");
1873
2244
  var import_core13 = require("@penvhq/core");
1874
2245
  var import_citty8 = require("citty");
@@ -1933,7 +2304,7 @@ function configOf(decisions) {
1933
2304
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
1934
2305
  }
1935
2306
  function declaredIn(root) {
1936
- const file = (0, import_node_path5.join)(root, CONFIG_FILE);
2307
+ const file = (0, import_node_path6.join)(root, CONFIG_FILE);
1937
2308
  if (!(0, import_node_fs3.existsSync)(file)) {
1938
2309
  return void 0;
1939
2310
  }
@@ -2329,7 +2700,7 @@ function renderTsconfig(target, alias) {
2329
2700
  `;
2330
2701
  }
2331
2702
  function ensurePenvDir(root) {
2332
- const dir = (0, import_node_path5.resolve)(root, PENV_DIR2);
2703
+ const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
2333
2704
  if ((0, import_node_fs3.existsSync)(dir)) {
2334
2705
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
2335
2706
  }
@@ -2337,7 +2708,7 @@ function ensurePenvDir(root) {
2337
2708
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
2338
2709
  }
2339
2710
  function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2340
- const file = (0, import_node_path5.join)(root, ...decisions.schemaFile.split("/"));
2711
+ const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
2341
2712
  if ((0, import_node_fs3.existsSync)(file)) {
2342
2713
  return {
2343
2714
  target: "schema",
@@ -2346,7 +2717,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2346
2717
  note: "(yours \u2014 penv never regenerates it)"
2347
2718
  };
2348
2719
  }
2349
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.dirname)(file), { recursive: true });
2720
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
2350
2721
  (0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
2351
2722
  return {
2352
2723
  target: "schema",
@@ -2356,7 +2727,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2356
2727
  };
2357
2728
  }
2358
2729
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
2359
- const file = (0, import_node_path5.join)(root, CONFIG_FILE);
2730
+ const file = (0, import_node_path6.join)(root, CONFIG_FILE);
2360
2731
  if ((0, import_node_fs3.existsSync)(file)) {
2361
2732
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
2362
2733
  }
@@ -2366,7 +2737,7 @@ function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
2366
2737
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
2367
2738
  const alias = decisions.alias;
2368
2739
  const imports = alias.startsWith(IMPORTS_PREFIX);
2369
- const file = (0, import_node_path5.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
2740
+ const file = (0, import_node_path6.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
2370
2741
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
2371
2742
  if (!(0, import_node_fs3.existsSync)(file)) {
2372
2743
  if (imports) {
@@ -2409,14 +2780,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
2409
2780
  };
2410
2781
  }
2411
2782
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
2412
- const file = (0, import_node_path5.join)(root, PENV_DIR2, GITIGNORE_FILE);
2783
+ const file = (0, import_node_path6.join)(root, PENV_DIR2, GITIGNORE_FILE);
2413
2784
  const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
2414
2785
  const wanted = renderGitignore(decisions);
2415
2786
  const existing = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : void 0;
2416
2787
  if (existing === wanted) {
2417
2788
  return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
2418
2789
  }
2419
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(root, PENV_DIR2), { recursive: true });
2790
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
2420
2791
  (0, import_node_fs3.writeFileSync)(file, wanted, "utf8");
2421
2792
  return {
2422
2793
  target: "gitignore",
@@ -2434,20 +2805,20 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2434
2805
  ];
2435
2806
  }
2436
2807
  function runInit(options) {
2437
- const root = (0, import_node_path5.resolve)(options.cwd);
2808
+ const root = (0, import_node_path6.resolve)(options.cwd);
2438
2809
  const decisions = options.decisions ?? planInit(root).decisions;
2439
2810
  return { root, decisions, steps: scaffold(root, [], false, decisions) };
2440
2811
  }
2441
- function renderInit(result) {
2442
- const steps = result.steps.map((step) => {
2812
+ function renderInit(result2) {
2813
+ const steps = result2.steps.map((step) => {
2443
2814
  const glyph = step.action === "conflicted" ? WARN : CHECK;
2444
2815
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
2445
2816
  });
2446
2817
  return [
2447
2818
  ...formatSteps(steps),
2448
2819
  "",
2449
- `Done. Declare your parameters in ${result.decisions.schemaFile}, then \`penv set <key>\`.`,
2450
- ...result.decisions.environments.length === 0 ? [
2820
+ `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
2821
+ ...result2.decisions.environments.length === 0 ? [
2451
2822
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2452
2823
  ] : []
2453
2824
  ];
@@ -2486,7 +2857,7 @@ var initCommand = (0, import_citty8.defineCommand)({
2486
2857
  },
2487
2858
  run({ args }) {
2488
2859
  return guard(async () => {
2489
- const root = (0, import_node_path5.resolve)(process.cwd());
2860
+ const root = (0, import_node_path6.resolve)(process.cwd());
2490
2861
  const environments = environmentsFromFlag(args.env);
2491
2862
  const plan2 = planInit(root, {
2492
2863
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -2577,7 +2948,7 @@ function assertDeclared(segment, config) {
2577
2948
  throw new import_core14.UnknownEnvironmentError(segment, config.environments);
2578
2949
  }
2579
2950
  function scopeFromFilename(file, config) {
2580
- const name = (0, import_node_path6.basename)(file);
2951
+ const name = (0, import_node_path7.basename)(file);
2581
2952
  const segments = name.split(".");
2582
2953
  const start = segments.indexOf(DOTENV_SEGMENT);
2583
2954
  if (start === -1) {
@@ -2661,7 +3032,7 @@ function environmentNamed(file, explicit) {
2661
3032
  if (explicit !== void 0 && explicit.trim().length > 0) {
2662
3033
  return explicit.trim();
2663
3034
  }
2664
- const segments = (0, import_node_path6.basename)(file).split(".");
3035
+ const segments = (0, import_node_path7.basename)(file).split(".");
2665
3036
  const start = segments.indexOf(DOTENV_SEGMENT);
2666
3037
  if (start === -1) {
2667
3038
  return void 0;
@@ -2692,8 +3063,8 @@ function configInEffect(cwd, environment) {
2692
3063
  return { config: openProject(cwd).config, decisions };
2693
3064
  }
2694
3065
  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);
3066
+ const cwd = (0, import_node_path7.resolve)(options.cwd);
3067
+ const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
2697
3068
  if (!(0, import_node_fs4.existsSync)(file)) {
2698
3069
  throw new import_core14.PenvError(
2699
3070
  "IMPORT_FILE_MISSING",
@@ -2752,7 +3123,7 @@ function importDotenv(options) {
2752
3123
  };
2753
3124
  }
2754
3125
  function displayPath2(root, file) {
2755
- const rel = (0, import_node_path6.relative)(root, file);
3126
+ const rel = (0, import_node_path7.relative)(root, file);
2756
3127
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
2757
3128
  }
2758
3129
  function keptSchemaStep(step, variables) {
@@ -2776,19 +3147,19 @@ function skippedValidationStep(environments) {
2776
3147
  note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
2777
3148
  };
2778
3149
  }
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";
3150
+ function renderImport(result2, validation) {
3151
+ const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
3152
+ if (result2.orphanComments > 0) {
3153
+ const plural = result2.orphanComments === 1 ? "comment" : "comments";
2783
3154
  steps.push({
2784
3155
  glyph: WARN,
2785
- text: `Dropped ${result.orphanComments} orphan ${plural}`,
3156
+ text: `Dropped ${result2.orphanComments} orphan ${plural}`,
2786
3157
  note: "attached to no variable, so nothing to describe"
2787
3158
  });
2788
3159
  }
2789
- for (const step of result.steps) {
3160
+ for (const step of result2.steps) {
2790
3161
  if (step.target === "schema" && step.action === "kept") {
2791
- steps.push(keptSchemaStep(step, result.variables));
3162
+ steps.push(keptSchemaStep(step, result2.variables));
2792
3163
  continue;
2793
3164
  }
2794
3165
  const glyph = step.action === "conflicted" ? WARN : CHECK;
@@ -2796,10 +3167,10 @@ function renderImport(result, validation) {
2796
3167
  step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
2797
3168
  );
2798
3169
  }
2799
- steps.push({ glyph: CHECK, text: `Created ${displayPath2(result.root, result.backup)}` });
3170
+ steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
2800
3171
  const lines = formatSteps(steps);
2801
3172
  if (validation === void 0) {
2802
- lines.push(...formatSteps([skippedValidationStep(result.environments)]));
3173
+ lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
2803
3174
  } else if (validation.ok) {
2804
3175
  lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
2805
3176
  } else {
@@ -2844,12 +3215,12 @@ var import_core15 = require("@penvhq/core");
2844
3215
  var import_citty10 = require("citty");
2845
3216
 
2846
3217
  // src/keychain.ts
2847
- var import_node_module = require("module");
3218
+ var import_node_module2 = require("module");
2848
3219
  var import_meta = {};
2849
3220
  var cached;
2850
3221
  function entryConstructor() {
2851
3222
  if (cached === void 0) {
2852
- const require2 = (0, import_node_module.createRequire)(import_meta.url);
3223
+ const require2 = (0, import_node_module2.createRequire)(import_meta.url);
2853
3224
  cached = require2("@napi-rs/keyring").Entry;
2854
3225
  }
2855
3226
  return cached;
@@ -2913,19 +3284,19 @@ function runKeyCreate(options) {
2913
3284
  key
2914
3285
  };
2915
3286
  }
2916
- function renderKeyCreate(result) {
2917
- if (result.source === "keychain") {
3287
+ function renderKeyCreate(result2) {
3288
+ if (result2.source === "keychain") {
2918
3289
  return [
2919
- `A new key for environment ${result.environment}, stored in your OS keychain as \`${result.id}\`.`,
3290
+ `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
2920
3291
  "",
2921
3292
  "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
2922
3293
  "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
2923
3294
  ];
2924
3295
  }
2925
3296
  return [
2926
- `A new key for environment ${result.environment}. penv did not store it.`,
3297
+ `A new key for environment ${result2.environment}. penv did not store it.`,
2927
3298
  "",
2928
- ` ${result.variable}=${result.key}`,
3299
+ ` ${result2.variable}=${result2.key}`,
2929
3300
  "",
2930
3301
  "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
2931
3302
  "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
@@ -2997,11 +3368,11 @@ async function runList(options) {
2997
3368
  }
2998
3369
  return { environment, parameters };
2999
3370
  }
3000
- function renderList(result) {
3001
- if (result.parameters.length === 0) {
3002
- return [`No parameters in ${PENV_DIR}/ for environment ${result.environment}.`];
3371
+ function renderList(result2) {
3372
+ if (result2.parameters.length === 0) {
3373
+ return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3003
3374
  }
3004
- return columns(result.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3375
+ return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3005
3376
  }
3006
3377
  var listCommand = (0, import_citty11.defineCommand)({
3007
3378
  meta: { name: "list", description: "List parameters" },
@@ -3011,11 +3382,11 @@ var listCommand = (0, import_citty11.defineCommand)({
3011
3382
  },
3012
3383
  run({ args }) {
3013
3384
  return guard(async () => {
3014
- const result = await runList({
3385
+ const result2 = await runList({
3015
3386
  cwd: process.cwd(),
3016
3387
  ...args.env === void 0 ? {} : { environment: args.env }
3017
3388
  });
3018
- write(args.json === true ? [JSON.stringify(result, null, 2)] : renderList(result));
3389
+ write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
3019
3390
  });
3020
3391
  }
3021
3392
  });
@@ -3127,20 +3498,20 @@ async function runMove(options) {
3127
3498
  schema: { was: (0, import_core17.accessPath)(from).join("."), now: (0, import_core17.accessPath)(to).join(".") }
3128
3499
  };
3129
3500
  }
3130
- function renderMove(result) {
3131
- const rows = result.files.map((file) => ({
3501
+ function renderMove(result2) {
3502
+ const rows = result2.files.map((file) => ({
3132
3503
  glyph: CHECK,
3133
3504
  label: "Moved",
3134
3505
  subject: `${PENV_DIR}/${file.to}`,
3135
3506
  ...file.resealed ? { detail: "re-sealed for its new address" } : {}
3136
3507
  }));
3137
- if (result.meta !== void 0) {
3138
- rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result.meta}` });
3508
+ if (result2.meta !== void 0) {
3509
+ rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
3139
3510
  }
3140
3511
  const lines = formatRows(rows);
3141
3512
  lines.push(
3142
3513
  "",
3143
- ` .penv/env.ts still declares \`${result.schema.was}\`. Rename it to \`${result.schema.now}\`,`,
3514
+ ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3144
3515
  " or `penv validate` will report the value as unused and the declaration as unset."
3145
3516
  );
3146
3517
  return lines;
@@ -3166,9 +3537,86 @@ var mvCommand = (0, import_citty12.defineCommand)({
3166
3537
  }
3167
3538
  });
3168
3539
 
3540
+ // src/commands/pull.ts
3541
+ var import_citty13 = require("citty");
3542
+ async function runPull(options) {
3543
+ const project = openProject(options.cwd);
3544
+ const environment = targetEnvironment(project, options.environment);
3545
+ const source = await sourceProviderFor(project, environment);
3546
+ if (source.type === LOCAL_TREE_TYPE) {
3547
+ return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
3548
+ }
3549
+ const tree = localTree(project);
3550
+ const files = await source.list();
3551
+ let values = 0;
3552
+ for (const file of files) {
3553
+ const value = await source.read(file);
3554
+ if (value === void 0) {
3555
+ continue;
3556
+ }
3557
+ tree.writeSync(file, value);
3558
+ values += 1;
3559
+ }
3560
+ const refs = refsFrom(files);
3561
+ let meta = 0;
3562
+ for (const ref of refs) {
3563
+ const block = await source.readMeta(ref);
3564
+ if (block === void 0) {
3565
+ continue;
3566
+ }
3567
+ tree.writeMetaSync(ref, block);
3568
+ meta += 1;
3569
+ }
3570
+ return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
3571
+ }
3572
+ function renderPull(result2) {
3573
+ if (result2.localSource) {
3574
+ return formatRows([
3575
+ {
3576
+ glyph: CHECK,
3577
+ label: "Nothing to pull",
3578
+ subject: `environment ${result2.environment} has no separate source of truth`,
3579
+ detail: "its values live in the local .penv tree already"
3580
+ }
3581
+ ]);
3582
+ }
3583
+ return formatRows([
3584
+ {
3585
+ glyph: CHECK,
3586
+ label: "Pulled",
3587
+ subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
3588
+ detail: `from the ${result2.source} provider for environment ${result2.environment}`
3589
+ },
3590
+ {
3591
+ glyph: CHECK,
3592
+ label: "Parameters",
3593
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
3594
+ detail: "written into the local .penv tree"
3595
+ }
3596
+ ]);
3597
+ }
3598
+ var pullCommand = (0, import_citty13.defineCommand)({
3599
+ meta: {
3600
+ name: "pull",
3601
+ description: "Materialise the local .penv tree from an environment's source-of-truth provider"
3602
+ },
3603
+ args: {
3604
+ env: { type: "string", description: "The environment to pull" }
3605
+ },
3606
+ run({ args }) {
3607
+ return guard(async () => {
3608
+ const result2 = await runPull({
3609
+ cwd: process.cwd(),
3610
+ ...args.env === void 0 ? {} : { environment: args.env }
3611
+ });
3612
+ write(renderPull(result2));
3613
+ });
3614
+ }
3615
+ });
3616
+
3169
3617
  // src/commands/remove.ts
3170
3618
  var import_core18 = require("@penvhq/core");
3171
- var import_citty13 = require("citty");
3619
+ var import_citty14 = require("citty");
3172
3620
  async function runRemove(options) {
3173
3621
  const project = openProject(options.cwd);
3174
3622
  const ref = refFromKey(options.key);
@@ -3193,9 +3641,9 @@ async function runRemove(options) {
3193
3641
  considered: files.map((file) => (0, import_core18.formatValueFile)(file))
3194
3642
  };
3195
3643
  }
3196
- function renderRemove(result) {
3197
- if (result.removed.length === 0) {
3198
- const first = result.considered[0] ?? result.parameter;
3644
+ function renderRemove(result2) {
3645
+ if (result2.removed.length === 0) {
3646
+ const first = result2.considered[0] ?? result2.parameter;
3199
3647
  return formatRows([
3200
3648
  {
3201
3649
  glyph: WARN,
@@ -3205,14 +3653,14 @@ function renderRemove(result) {
3205
3653
  }
3206
3654
  ]);
3207
3655
  }
3208
- const rows = result.removed.map((location) => ({
3656
+ const rows = result2.removed.map((location) => ({
3209
3657
  glyph: CHECK,
3210
3658
  label: "Removed",
3211
3659
  subject: `${PENV_DIR}/${location}`
3212
3660
  }));
3213
3661
  return formatRows(rows);
3214
3662
  }
3215
- var removeCommand = (0, import_citty13.defineCommand)({
3663
+ var removeCommand = (0, import_citty14.defineCommand)({
3216
3664
  meta: { name: "remove", description: "Delete a parameter" },
3217
3665
  args: {
3218
3666
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -3238,15 +3686,195 @@ var removeCommand = (0, import_citty13.defineCommand)({
3238
3686
  }
3239
3687
  });
3240
3688
 
3689
+ // src/commands/rotate.ts
3690
+ var import_core19 = require("@penvhq/core");
3691
+ var import_citty15 = require("citty");
3692
+ function rotatingFile(ref, environment) {
3693
+ return {
3694
+ namespace: ref.namespace,
3695
+ name: ref.name,
3696
+ scope: { kind: "environment", environment },
3697
+ encrypted: false
3698
+ };
3699
+ }
3700
+ async function writeRotatedValue(project, provider, ref, environment, value) {
3701
+ if (provider.type === LOCAL_TREE_TYPE) {
3702
+ await sealAwareWrite({
3703
+ project,
3704
+ provider,
3705
+ ref,
3706
+ scope: { kind: "environment", environment },
3707
+ value,
3708
+ environment
3709
+ });
3710
+ return;
3711
+ }
3712
+ await provider.write(rotatingFile(ref, environment), value);
3713
+ }
3714
+ function requireNewValue(value, phase, key) {
3715
+ if (value === void 0) {
3716
+ throw new import_core19.PenvError(
3717
+ "ROTATION_NO_VALUE",
3718
+ `A ${phase} rotation of ${key} writes a new value, and none was given`,
3719
+ "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
3720
+ );
3721
+ }
3722
+ return value;
3723
+ }
3724
+ function requireRetaining(provider, environment) {
3725
+ if (!(0, import_core19.retainsPrevious)(provider)) {
3726
+ throw new import_core19.PenvError(
3727
+ "ROTATION_NOT_RETAINING",
3728
+ `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`,
3729
+ "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."
3730
+ );
3731
+ }
3732
+ return provider;
3733
+ }
3734
+ async function runRotate(options) {
3735
+ const project = openProject(options.cwd);
3736
+ const environment = targetEnvironment(project, options.environment);
3737
+ const ref = refFromKey(options.key, project.config);
3738
+ const provider = await sourceProviderFor(project, environment);
3739
+ const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
3740
+ const before = await provider.readMeta(ref);
3741
+ const { mechanism } = (0, import_core19.rotationOf)(before, environment);
3742
+ if (mechanism === void 0) {
3743
+ throw new import_core19.PenvError(
3744
+ "ROTATION_NO_MECHANISM",
3745
+ `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
3746
+ '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.'
3747
+ );
3748
+ }
3749
+ const begin = options.begin === true;
3750
+ const complete = options.complete === true;
3751
+ if (mechanism === "atomic-cutover") {
3752
+ if (begin || complete) {
3753
+ throw new import_core19.PenvError(
3754
+ "ROTATION_MECHANISM_MISMATCH",
3755
+ `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
3756
+ "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."
3757
+ );
3758
+ }
3759
+ const value = requireNewValue(options.value, "cutover", options.key);
3760
+ await writeRotatedValue(project, provider, ref, environment, value);
3761
+ const after2 = (0, import_core19.completeRotation)(before, environment, nowIso);
3762
+ await provider.writeMeta(ref, after2);
3763
+ return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
3764
+ }
3765
+ const retaining = requireRetaining(provider, environment);
3766
+ if (begin === complete) {
3767
+ throw new import_core19.PenvError(
3768
+ "ROTATION_PHASE_REQUIRED",
3769
+ `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
3770
+ "`--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."
3771
+ );
3772
+ }
3773
+ if (begin) {
3774
+ const value = requireNewValue(options.value, "begin", options.key);
3775
+ await writeRotatedValue(project, retaining, ref, environment, value);
3776
+ const after2 = (0, import_core19.beginRotation)(before, environment, nowIso);
3777
+ await retaining.writeMeta(ref, after2);
3778
+ return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
3779
+ }
3780
+ const after = (0, import_core19.completeRotation)(before, environment, nowIso);
3781
+ await retaining.writeMeta(ref, after);
3782
+ return result(ref, environment, mechanism, "complete", retaining.type, false, after);
3783
+ }
3784
+ function result(ref, environment, mechanism, phase, source, wroteValue, after) {
3785
+ const { state, rotatingSince, lastRotated } = (0, import_core19.rotationOf)(after, environment);
3786
+ return {
3787
+ parameter: [...ref.namespace, ref.name].join("/"),
3788
+ environment,
3789
+ mechanism,
3790
+ phase,
3791
+ source,
3792
+ wroteValue,
3793
+ state,
3794
+ rotatingSince,
3795
+ lastRotated
3796
+ };
3797
+ }
3798
+ function renderRotate(result2) {
3799
+ if (result2.phase === "begin") {
3800
+ return formatRows([
3801
+ {
3802
+ glyph: CHECK,
3803
+ label: "Rotating",
3804
+ subject: result2.parameter,
3805
+ detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
3806
+ },
3807
+ {
3808
+ glyph: CHECK,
3809
+ label: "Previous",
3810
+ subject: "still readable",
3811
+ detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
3812
+ }
3813
+ ]);
3814
+ }
3815
+ if (result2.phase === "complete") {
3816
+ return formatRows([
3817
+ {
3818
+ glyph: CHECK,
3819
+ label: "Rotated",
3820
+ subject: result2.parameter,
3821
+ detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
3822
+ }
3823
+ ]);
3824
+ }
3825
+ return formatRows([
3826
+ {
3827
+ glyph: CHECK,
3828
+ label: "Rotated",
3829
+ subject: result2.parameter,
3830
+ detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
3831
+ }
3832
+ ]);
3833
+ }
3834
+ var rotateCommand = (0, import_citty15.defineCommand)({
3835
+ meta: {
3836
+ name: "rotate",
3837
+ description: "Rotate a parameter's value by the mechanism its meta declares"
3838
+ },
3839
+ args: {
3840
+ key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
3841
+ value: {
3842
+ type: "positional",
3843
+ required: false,
3844
+ description: "The new value; read from stdin if omitted. Not needed with --complete"
3845
+ },
3846
+ env: { type: "string", description: "The environment to rotate in" },
3847
+ begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
3848
+ complete: { type: "boolean", description: "Close a dual-valid grace window" }
3849
+ },
3850
+ run({ args }) {
3851
+ return guard(async () => {
3852
+ const value = args.complete === true ? void 0 : args.value ?? await readStdin();
3853
+ write(
3854
+ renderRotate(
3855
+ await runRotate({
3856
+ cwd: process.cwd(),
3857
+ key: args.key,
3858
+ ...value === void 0 ? {} : { value },
3859
+ ...args.env === void 0 ? {} : { environment: args.env },
3860
+ ...args.begin === void 0 ? {} : { begin: args.begin },
3861
+ ...args.complete === void 0 ? {} : { complete: args.complete }
3862
+ })
3863
+ )
3864
+ );
3865
+ });
3866
+ }
3867
+ });
3868
+
3241
3869
  // src/commands/watch.ts
3242
3870
  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");
3871
+ var import_node_path8 = require("path");
3872
+ var import_core20 = require("@penvhq/core");
3873
+ var import_citty16 = require("citty");
3246
3874
  var DEBOUNCE_MS = 100;
3247
3875
  function runWatch(options) {
3248
3876
  const project = openProject(options.cwd);
3249
- const configFile = (0, import_node_path7.basename)(project.configFile);
3877
+ const configFile = (0, import_node_path8.basename)(project.configFile);
3250
3878
  const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
3251
3879
  const watchers = /* @__PURE__ */ new Set();
3252
3880
  let timer;
@@ -3263,12 +3891,12 @@ function runWatch(options) {
3263
3891
  }
3264
3892
  running = true;
3265
3893
  try {
3266
- const result = await runValidate({
3894
+ const result2 = await runValidate({
3267
3895
  cwd: options.cwd,
3268
3896
  ...options.environment === void 0 ? {} : { environment: options.environment }
3269
3897
  });
3270
3898
  if (!closed) {
3271
- options.onResult?.(result);
3899
+ options.onResult?.(result2);
3272
3900
  }
3273
3901
  } catch (error) {
3274
3902
  if (!closed) {
@@ -3299,8 +3927,8 @@ function runWatch(options) {
3299
3927
  watcher.close();
3300
3928
  }
3301
3929
  function armRecovery(target, recursive, only) {
3302
- const parent = (0, import_node_path7.dirname)(target);
3303
- const name = (0, import_node_path7.basename)(target);
3930
+ const parent = (0, import_node_path8.dirname)(target);
3931
+ const name = (0, import_node_path8.basename)(target);
3304
3932
  let recovery;
3305
3933
  try {
3306
3934
  recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
@@ -3311,7 +3939,7 @@ function runWatch(options) {
3311
3939
  stop(recovery);
3312
3940
  return;
3313
3941
  }
3314
- if (filename !== null && (0, import_node_path7.basename)(filename) !== name) {
3942
+ if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
3315
3943
  return;
3316
3944
  }
3317
3945
  if (!(0, import_node_fs5.existsSync)(target)) {
@@ -3346,7 +3974,7 @@ function runWatch(options) {
3346
3974
  schedule();
3347
3975
  return;
3348
3976
  }
3349
- if (only !== void 0 && (filename === null || (0, import_node_path7.basename)(filename) !== only)) {
3977
+ if (only !== void 0 && (filename === null || (0, import_node_path8.basename)(filename) !== only)) {
3350
3978
  return;
3351
3979
  }
3352
3980
  schedule();
@@ -3373,10 +4001,10 @@ function runWatch(options) {
3373
4001
  watchers.add(watcher);
3374
4002
  }
3375
4003
  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));
4004
+ addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
4005
+ if ((0, import_core20.schemaInsideTree)(project.config) === void 0) {
4006
+ const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core20.schemaFileOf)(project.config));
4007
+ addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
3380
4008
  }
3381
4009
  void validate();
3382
4010
  return {
@@ -3392,8 +4020,8 @@ function runWatch(options) {
3392
4020
  }
3393
4021
  };
3394
4022
  }
3395
- function renderWatch(result) {
3396
- return ["", ...renderValidate(result), ...renderDrift(result.drift, result.environment)];
4023
+ function renderWatch(result2) {
4024
+ return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
3397
4025
  }
3398
4026
  function renderDrift(drift, environment) {
3399
4027
  const rows = [
@@ -3419,7 +4047,7 @@ function renderDrift(drift, environment) {
3419
4047
  }
3420
4048
  return lines;
3421
4049
  }
3422
- var watchCommand = (0, import_citty14.defineCommand)({
4050
+ var watchCommand = (0, import_citty16.defineCommand)({
3423
4051
  meta: {
3424
4052
  name: "watch",
3425
4053
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -3432,8 +4060,8 @@ var watchCommand = (0, import_citty14.defineCommand)({
3432
4060
  const handle = runWatch({
3433
4061
  cwd: process.cwd(),
3434
4062
  ...args.env === void 0 ? {} : { environment: args.env },
3435
- onResult: (result) => {
3436
- write(renderWatch(result));
4063
+ onResult: (result2) => {
4064
+ write(renderWatch(result2));
3437
4065
  },
3438
4066
  // A failing cycle is reported and watching continues. `reportError`
3439
4067
  // marks the process failed, which `validate` wants and a loop does not:
@@ -3447,10 +4075,10 @@ var watchCommand = (0, import_citty14.defineCommand)({
3447
4075
  }
3448
4076
  });
3449
4077
  write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
3450
- await new Promise((resolve6) => {
4078
+ await new Promise((resolve7) => {
3451
4079
  process.once("SIGINT", () => {
3452
4080
  handle.close();
3453
- resolve6();
4081
+ resolve7();
3454
4082
  });
3455
4083
  });
3456
4084
  });
@@ -3458,7 +4086,7 @@ var watchCommand = (0, import_citty14.defineCommand)({
3458
4086
  });
3459
4087
 
3460
4088
  // src/index.ts
3461
- var main = (0, import_citty15.defineCommand)({
4089
+ var main = (0, import_citty17.defineCommand)({
3462
4090
  meta: {
3463
4091
  name: "penv",
3464
4092
  description: "Configuration that shares a data model with your production secret manager"
@@ -3470,7 +4098,9 @@ var main = (0, import_citty15.defineCommand)({
3470
4098
  get: getCommand,
3471
4099
  set: setCommand,
3472
4100
  mv: mvCommand,
4101
+ pull: pullCommand,
3473
4102
  push: pushCommand,
4103
+ rotate: rotateCommand,
3474
4104
  remove: removeCommand,
3475
4105
  list: listCommand,
3476
4106
  encrypt: encryptCommand,
@@ -3482,8 +4112,8 @@ var main = (0, import_citty15.defineCommand)({
3482
4112
  }
3483
4113
  });
3484
4114
  function runMain() {
3485
- (0, import_core20.setKeychain)(defaultKeychain);
3486
- return (0, import_citty15.runMain)(main);
4115
+ (0, import_core21.setKeychain)(defaultKeychain);
4116
+ return (0, import_citty17.runMain)(main);
3487
4117
  }
3488
4118
  // Annotate the CommonJS export names for ESM import in node:
3489
4119
  0 && (module.exports = {
@@ -3494,7 +4124,9 @@ function runMain() {
3494
4124
  main,
3495
4125
  renderDoctor,
3496
4126
  renderMove,
4127
+ renderPull,
3497
4128
  renderPush,
4129
+ renderRotate,
3498
4130
  renderWatch,
3499
4131
  runDecrypt,
3500
4132
  runDoctor,
@@ -3506,8 +4138,10 @@ function runMain() {
3506
4138
  runList,
3507
4139
  runMain,
3508
4140
  runMove,
4141
+ runPull,
3509
4142
  runPush,
3510
4143
  runRemove,
4144
+ runRotate,
3511
4145
  runSet,
3512
4146
  runValidate,
3513
4147
  runWatch