@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.js CHANGED
@@ -1,22 +1,28 @@
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 defineCommand17 } 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,
@@ -32,12 +38,30 @@ import {
32
38
  import { FilesystemProvider } from "@penvhq/provider-filesystem";
33
39
 
34
40
  // src/registry.ts
41
+ import { createRequire } from "module";
42
+ import { dirname, resolve } from "path";
43
+ import { pathToFileURL } from "url";
35
44
  import { PenvError } from "@penvhq/core";
36
45
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
46
+ import { createMockProvider } from "@penvhq/provider-mock";
47
+ import { createVaultProvider } from "@penvhq/provider-vault";
48
+ var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
37
49
  var LOCAL_TREE_TYPE = "filesystem";
38
50
  var REGISTRY = /* @__PURE__ */ new Map([
39
- [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })]
51
+ [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
52
+ ["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
53
+ ["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
40
54
  ]);
55
+ var CONTRACT_METHODS = [
56
+ "read",
57
+ "write",
58
+ "list",
59
+ "remove",
60
+ "readMeta",
61
+ "writeMeta",
62
+ "removeMeta"
63
+ ];
64
+ var pluginModuleCache = /* @__PURE__ */ new Map();
41
65
  function isProviderRegistered(type) {
42
66
  return REGISTRY.has(type);
43
67
  }
@@ -48,20 +72,94 @@ function createProvider(type, context) {
48
72
  }
49
73
  return factory(context);
50
74
  }
51
- function assertProvidersRegistered(config) {
75
+ async function createSourceProvider(type, context) {
76
+ if (REGISTRY.has(type)) {
77
+ return createProvider(type, context);
78
+ }
79
+ return loadPluginProvider(type, context);
80
+ }
81
+ async function loadPluginProvider(type, context) {
82
+ const fromDir = resolutionBase(context);
83
+ const specifier = pluginSpecifier(context.providerConfig, type);
84
+ const resolved = resolvePlugin(specifier, fromDir);
85
+ if (resolved === void 0) {
86
+ throw unknownProvider(type, context.environment, specifier);
87
+ }
88
+ let mod;
89
+ try {
90
+ mod = await importPlugin(resolved);
91
+ } catch {
92
+ throw new PenvError(
93
+ "PROVIDER_PLUGIN_LOAD",
94
+ `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
95
+ "It resolved but threw while importing. Check it builds and its dependencies are installed."
96
+ );
97
+ }
98
+ const factory = mod[PLUGIN_FACTORY_EXPORT];
99
+ if (typeof factory !== "function") {
100
+ throw new PenvError(
101
+ "PROVIDER_PLUGIN_INVALID",
102
+ `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
103
+ `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
104
+ );
105
+ }
106
+ const provider = await factory(context);
107
+ assertSatisfiesContract(provider, specifier);
108
+ return provider;
109
+ }
110
+ function assertProvidersRegistered(config, projectRoot) {
52
111
  for (const [environment, provider] of Object.entries(config.providers)) {
53
- if (!isProviderRegistered(provider.type)) {
54
- throw unknownProvider(provider.type, environment);
112
+ if (isProviderRegistered(provider.type)) {
113
+ continue;
114
+ }
115
+ const specifier = pluginSpecifier(provider, provider.type);
116
+ if (resolvePlugin(specifier, projectRoot) === void 0) {
117
+ throw unknownProvider(provider.type, environment, specifier);
55
118
  }
56
119
  }
57
120
  }
58
- function unknownProvider(type, environment) {
121
+ function pluginSpecifier(providerConfig, type) {
122
+ return providerConfig?.module ?? `@penvhq/provider-${type}`;
123
+ }
124
+ function resolutionBase(context) {
125
+ return dirname(context.root);
126
+ }
127
+ function resolvePlugin(specifier, fromDir) {
128
+ try {
129
+ const require2 = createRequire(resolve(fromDir, "noop.js"));
130
+ return require2.resolve(specifier);
131
+ } catch {
132
+ return void 0;
133
+ }
134
+ }
135
+ function importPlugin(resolvedPath) {
136
+ const cached2 = pluginModuleCache.get(resolvedPath);
137
+ if (cached2 !== void 0) {
138
+ return cached2;
139
+ }
140
+ const loading = import(pathToFileURL(resolvedPath).href);
141
+ pluginModuleCache.set(resolvedPath, loading);
142
+ return loading;
143
+ }
144
+ function assertSatisfiesContract(provider, specifier) {
145
+ for (const method of CONTRACT_METHODS) {
146
+ if (typeof provider[method] !== "function") {
147
+ throw new PenvError(
148
+ "PROVIDER_PLUGIN_INVALID",
149
+ `The provider from \`${specifier}\` is missing \`${method}()\``,
150
+ "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
151
+ );
152
+ }
153
+ }
154
+ }
155
+ function unknownProvider(type, environment, specifier) {
59
156
  const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
60
157
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
158
+ 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
159
  return new PenvError(
62
160
  "UNKNOWN_PROVIDER",
63
161
  `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}\`.`
162
+ remedy
65
163
  );
66
164
  }
67
165
 
@@ -69,9 +167,9 @@ function unknownProvider(type, environment) {
69
167
  var PENV_DIR = ".penv";
70
168
  function openProject(cwd) {
71
169
  const { config, file } = loadConfig(cwd);
72
- const root = dirname(file);
73
- const penvDir = resolve(root, PENV_DIR);
74
- assertProvidersRegistered(config);
170
+ const root = dirname2(file);
171
+ const penvDir = resolve2(root, PENV_DIR);
172
+ assertProvidersRegistered(config, root);
75
173
  return {
76
174
  root,
77
175
  configFile: file,
@@ -90,6 +188,18 @@ function localTree(project) {
90
188
  }
91
189
  return project.provider;
92
190
  }
191
+ async function sourceProviderFor(project, environment) {
192
+ const providerConfig = project.config.providers[environment];
193
+ if (providerConfig === void 0) {
194
+ return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
195
+ }
196
+ return createSourceProvider(providerConfig.type, {
197
+ root: project.penvDir,
198
+ config: project.config,
199
+ providerConfig,
200
+ environment
201
+ });
202
+ }
93
203
  function targetEnvironment(project, explicit) {
94
204
  return resolveEnvironment(project.config, explicit);
95
205
  }
@@ -471,36 +581,36 @@ async function runPush(options) {
471
581
  decrypted: outbound.filter((item) => item.encrypted).length
472
582
  };
473
583
  }
474
- function renderPush(result) {
475
- if (result.pushed === 0) {
584
+ function renderPush(result2) {
585
+ if (result2.pushed === 0) {
476
586
  return formatRows([
477
587
  {
478
588
  glyph: CHECK,
479
589
  label: "Nothing to push",
480
- subject: `no values resolve for environment ${result.environment}`
590
+ subject: `no values resolve for environment ${result2.environment}`
481
591
  }
482
592
  ]);
483
593
  }
484
- const target = `GitHub Actions for environment ${result.environment}${result.repo === void 0 ? "" : ` (${result.repo})`}`;
594
+ const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
485
595
  const rows = [
486
596
  {
487
597
  glyph: CHECK,
488
598
  label: "Pushed",
489
- subject: `${result.pushed} ${result.pushed === 1 ? "secret" : "secrets"}`,
599
+ subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
490
600
  detail: `to ${target}`
491
601
  },
492
602
  {
493
603
  glyph: CHECK,
494
604
  label: "Scopes",
495
- subject: `${result.environmentSecrets} environment, ${result.repositorySecrets} repository`,
605
+ subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
496
606
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
497
607
  }
498
608
  ];
499
- if (result.decrypted > 0) {
609
+ if (result2.decrypted > 0) {
500
610
  rows.push({
501
611
  glyph: WARN,
502
612
  label: "Decrypted",
503
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
613
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
504
614
  detail: "sent as plaintext for GitHub to re-seal under its own key"
505
615
  });
506
616
  }
@@ -520,19 +630,19 @@ var pushCommand = defineCommand({
520
630
  },
521
631
  run({ args }) {
522
632
  return guard(async () => {
523
- const result = await runPush({
633
+ const result2 = await runPush({
524
634
  cwd: process.cwd(),
525
635
  ...args.env === void 0 ? {} : { environment: args.env },
526
636
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
527
637
  });
528
- write(renderPush(result));
638
+ write(renderPush(result2));
529
639
  });
530
640
  }
531
641
  });
532
642
 
533
643
  // src/commands/validate.ts
534
644
  import { resolve as resolvePath } from "path";
535
- import { pathToFileURL } from "url";
645
+ import { pathToFileURL as pathToFileURL2 } from "url";
536
646
  import {
537
647
  accessPath as accessPath2,
538
648
  checkNameCollisions as checkNameCollisions2,
@@ -610,16 +720,16 @@ function validationIssuesOf(cause, schemaPath) {
610
720
  }
611
721
  var schemaLoads = Promise.resolve();
612
722
  function exclusively(work) {
613
- const result = schemaLoads.then(work, work);
614
- schemaLoads = result.then(
723
+ const result2 = schemaLoads.then(work, work);
724
+ schemaLoads = result2.then(
615
725
  () => void 0,
616
726
  () => void 0
617
727
  );
618
- return result;
728
+ return result2;
619
729
  }
620
730
  function loadSchema(project, environment) {
621
731
  const schemaPath = schemaFileOf(project.config);
622
- const file = pathToFileURL(resolvePath(project.root, schemaPath)).href;
732
+ const file = pathToFileURL2(resolvePath(project.root, schemaPath)).href;
623
733
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
624
734
  }
625
735
  async function loadSchemaExclusively(file, schemaPath, environment) {
@@ -721,9 +831,9 @@ async function runValidate(options) {
721
831
  }
722
832
  }
723
833
  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) {
834
+ const result2 = schema.safeParse(object);
835
+ if (!result2.success) {
836
+ for (const problem of result2.error.issues) {
727
837
  const path = problem.path.join(".");
728
838
  if (undecryptable.has(path)) {
729
839
  continue;
@@ -739,24 +849,24 @@ async function runValidate(options) {
739
849
  }
740
850
  return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
741
851
  }
742
- function renderValidate(result) {
743
- if (result.ok) {
852
+ function renderValidate(result2) {
853
+ if (result2.ok) {
744
854
  return formatRows([
745
855
  {
746
856
  glyph: CHECK,
747
857
  label: "Schema valid",
748
- subject: `${result.parameters} parameters for environment ${result.environment}`
858
+ subject: `${result2.parameters} parameters for environment ${result2.environment}`
749
859
  }
750
860
  ]);
751
861
  }
752
- const rows = result.issues.map((issue) => ({
862
+ const rows = result2.issues.map((issue) => ({
753
863
  glyph: WARN,
754
864
  label: LABELS[issue.kind],
755
865
  subject: issue.subject,
756
866
  detail: issue.message
757
867
  }));
758
868
  const lines = formatRows(rows);
759
- const remedies = [...new Set(result.issues.map((issue) => issue.remedy))];
869
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
760
870
  for (const remedy of remedies) {
761
871
  if (remedy !== void 0) {
762
872
  lines.push(` ${remedy}`);
@@ -774,12 +884,12 @@ var validateCommand = defineCommand2({
774
884
  },
775
885
  run({ args }) {
776
886
  return guard(async () => {
777
- const result = await runValidate({
887
+ const result2 = await runValidate({
778
888
  cwd: process.cwd(),
779
889
  ...args.env === void 0 ? {} : { environment: args.env }
780
890
  });
781
- write(renderValidate(result));
782
- if (!result.ok) {
891
+ write(renderValidate(result2));
892
+ if (!result2.ok) {
783
893
  process.exitCode = 1;
784
894
  }
785
895
  });
@@ -787,6 +897,7 @@ var validateCommand = defineCommand2({
787
897
  });
788
898
 
789
899
  // src/commands/doctor.ts
900
+ var STUCK_THRESHOLD_MS = 864e5;
790
901
  function skipped(check, label) {
791
902
  return {
792
903
  check,
@@ -800,6 +911,8 @@ async function runDoctor(options) {
800
911
  const project = openProject(options.cwd);
801
912
  const environment = targetEnvironment(project, options.environment);
802
913
  const findings = [];
914
+ const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
915
+ const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
803
916
  const { schema, issues } = await loadSchema(project, environment);
804
917
  if (schema !== void 0) {
805
918
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -848,6 +961,14 @@ async function runDoctor(options) {
848
961
  findings.push(...plaintextSecretFindings(subjects, environment));
849
962
  findings.push(...publicSecretFindings(subjects, environment, project.config));
850
963
  findings.push(...encryptionFindings(subjects, environment));
964
+ const rotation = await rotationSubjects(project, environment, subjects, options.source);
965
+ if (rotation.kind === "unreachable") {
966
+ findings.push(rotation.finding);
967
+ } else {
968
+ findings.push(...overdueFindings(rotation.subjects, environment, now));
969
+ findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
970
+ }
971
+ findings.push(...await providerDriftFindings(project, environment, options.source));
851
972
  findings.push(...await sinkFindings(project, environment, options.sink));
852
973
  findings.push({
853
974
  check: "provider",
@@ -1265,6 +1386,246 @@ async function sinkFindings(project, environment, override) {
1265
1386
  });
1266
1387
  return findings;
1267
1388
  }
1389
+ function humanizeMs(ms) {
1390
+ const abs = Math.max(0, ms);
1391
+ const day = 864e5;
1392
+ const hour = 36e5;
1393
+ const minute = 6e4;
1394
+ const round = (value, unit) => {
1395
+ const n = Math.round(value);
1396
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
1397
+ };
1398
+ if (abs >= day) return round(abs / day, "day");
1399
+ if (abs >= hour) return round(abs / hour, "hour");
1400
+ if (abs >= minute) return round(abs / minute, "minute");
1401
+ return round(abs / 1e3, "second");
1402
+ }
1403
+ function refPathOf(ref) {
1404
+ return [...ref.namespace, ref.name].join("/");
1405
+ }
1406
+ async function rotationSubjects(project, environment, local, override) {
1407
+ const providerConfig = project.config.providers[environment];
1408
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
1409
+ return { kind: "read", subjects: local };
1410
+ }
1411
+ const source = override ?? await sourceProviderFor(project, environment);
1412
+ try {
1413
+ const subjects = await Promise.all(
1414
+ local.map(async ({ resolution }) => ({
1415
+ resolution,
1416
+ meta: await source.readMeta(resolution.ref)
1417
+ }))
1418
+ );
1419
+ return { kind: "read", subjects };
1420
+ } catch (error) {
1421
+ return {
1422
+ kind: "unreachable",
1423
+ finding: {
1424
+ check: "rotation-overdue",
1425
+ severity: "unknown",
1426
+ label: "Rotation",
1427
+ subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
1428
+ detail: errorDetail(error)
1429
+ }
1430
+ };
1431
+ }
1432
+ }
1433
+ function overdueFindings(subjects, environment, now) {
1434
+ const findings = [];
1435
+ for (const { resolution, meta } of subjects) {
1436
+ const { policy, lastRotated } = rotationOf(meta, environment);
1437
+ if (policy === void 0 || lastRotated === null) {
1438
+ continue;
1439
+ }
1440
+ const interval = tryParseDuration(policy);
1441
+ if (interval === void 0) {
1442
+ findings.push({
1443
+ check: "rotation-overdue",
1444
+ severity: "warning",
1445
+ label: "Rotation policy invalid",
1446
+ subject: resolution.parameter,
1447
+ detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
1448
+ });
1449
+ continue;
1450
+ }
1451
+ const last = Date.parse(lastRotated);
1452
+ if (Number.isNaN(last)) {
1453
+ continue;
1454
+ }
1455
+ const overdueBy = now.getTime() - last - interval;
1456
+ if (overdueBy <= 0) {
1457
+ continue;
1458
+ }
1459
+ findings.push({
1460
+ check: "rotation-overdue",
1461
+ severity: "warning",
1462
+ label: "Rotation overdue",
1463
+ subject: resolution.parameter,
1464
+ detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
1465
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
1466
+ });
1467
+ }
1468
+ if (findings.length > 0) {
1469
+ return findings;
1470
+ }
1471
+ return [
1472
+ {
1473
+ check: "rotation-overdue",
1474
+ severity: "pass",
1475
+ label: "Rotation freshness",
1476
+ subject: `no parameter is past its rotation policy for ${environment}`
1477
+ }
1478
+ ];
1479
+ }
1480
+ function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1481
+ const findings = [];
1482
+ for (const { resolution, meta } of subjects) {
1483
+ if (!isStuck(meta, environment, now, stuckThresholdMs)) {
1484
+ continue;
1485
+ }
1486
+ const { rotatingSince } = rotationOf(meta, environment);
1487
+ if (rotatingSince === null) {
1488
+ continue;
1489
+ }
1490
+ const openFor = now.getTime() - Date.parse(rotatingSince);
1491
+ findings.push({
1492
+ check: "rotation-stuck",
1493
+ severity: "warning",
1494
+ label: "Rotation stuck",
1495
+ subject: resolution.parameter,
1496
+ detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
1497
+ remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
1498
+ });
1499
+ }
1500
+ if (findings.length > 0) {
1501
+ return findings;
1502
+ }
1503
+ return [
1504
+ {
1505
+ check: "rotation-stuck",
1506
+ severity: "pass",
1507
+ label: "Rotation progress",
1508
+ subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
1509
+ }
1510
+ ];
1511
+ }
1512
+ function driftKey(file) {
1513
+ return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
1514
+ }
1515
+ function scopeKey(scope) {
1516
+ switch (scope.kind) {
1517
+ case "unscoped":
1518
+ return "unscoped";
1519
+ case "environment":
1520
+ return `environment:${scope.environment}`;
1521
+ case "local":
1522
+ return "local";
1523
+ case "environment-local":
1524
+ return `environment-local:${scope.environment}`;
1525
+ default:
1526
+ return assertNever(scope, "scope");
1527
+ }
1528
+ }
1529
+ function relevantToEnvironment(file, environment) {
1530
+ const scope = file.scope;
1531
+ if (scope.kind === "unscoped") return true;
1532
+ if (scope.kind === "environment") return scope.environment === environment;
1533
+ return false;
1534
+ }
1535
+ async function readRelevant(provider, environment) {
1536
+ const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
1537
+ const stored = await Promise.all(files.map((file) => provider.read(file)));
1538
+ const entries = /* @__PURE__ */ new Map();
1539
+ for (const [index, file] of files.entries()) {
1540
+ const value = stored[index];
1541
+ if (value !== void 0) {
1542
+ entries.set(driftKey(file), { file, stored: value });
1543
+ }
1544
+ }
1545
+ return entries;
1546
+ }
1547
+ async function providerDriftFindings(project, environment, override) {
1548
+ const providerConfig = project.config.providers[environment];
1549
+ if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
1550
+ return [
1551
+ {
1552
+ check: "provider-value-drift",
1553
+ severity: "pass",
1554
+ label: "Provider values",
1555
+ subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
1556
+ }
1557
+ ];
1558
+ }
1559
+ const source = override ?? await sourceProviderFor(project, environment);
1560
+ let local;
1561
+ let remote;
1562
+ try {
1563
+ local = await readRelevant(project.provider, environment);
1564
+ remote = await readRelevant(source, environment);
1565
+ } catch (error) {
1566
+ return [
1567
+ {
1568
+ check: "provider-value-drift",
1569
+ severity: "unknown",
1570
+ label: "Provider values",
1571
+ subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
1572
+ detail: errorDetail(error)
1573
+ }
1574
+ ];
1575
+ }
1576
+ const keys = keySourceFor(project, environment);
1577
+ const findings = [];
1578
+ const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
1579
+ for (const address of addresses) {
1580
+ const here = local.get(address);
1581
+ const there = remote.get(address);
1582
+ if (here !== void 0 && there !== void 0) {
1583
+ const opened = openValue2(here.file, here.stored, keys);
1584
+ if (opened.kind !== "plaintext") {
1585
+ findings.push({
1586
+ check: "provider-value-drift",
1587
+ severity: "unknown",
1588
+ label: "Provider value unreadable",
1589
+ subject: formatValueFile2(here.file),
1590
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
1591
+ });
1592
+ continue;
1593
+ }
1594
+ if (opened.value !== there.stored) {
1595
+ findings.push({
1596
+ check: "provider-value-drift",
1597
+ severity: "failure",
1598
+ label: "Provider value drift",
1599
+ subject: formatValueFile2(here.file),
1600
+ detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
1601
+ });
1602
+ }
1603
+ continue;
1604
+ }
1605
+ const present = here ?? there;
1606
+ if (present === void 0) {
1607
+ continue;
1608
+ }
1609
+ findings.push({
1610
+ check: "provider-value-drift",
1611
+ severity: "warning",
1612
+ label: here !== void 0 ? "Only in the local tree" : "Only in the source",
1613
+ subject: formatValueFile2(present.file),
1614
+ 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`
1615
+ });
1616
+ }
1617
+ if (findings.length > 0) {
1618
+ return findings;
1619
+ }
1620
+ return [
1621
+ {
1622
+ check: "provider-value-drift",
1623
+ severity: "pass",
1624
+ label: "Provider values",
1625
+ subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
1626
+ }
1627
+ ];
1628
+ }
1268
1629
  function renderDoctor(report) {
1269
1630
  const rows = report.findings.map((finding) => ({
1270
1631
  glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
@@ -1307,9 +1668,9 @@ var doctorCommand = defineCommand3({
1307
1668
 
1308
1669
  // src/commands/encrypt.ts
1309
1670
  import {
1310
- formatValueFile as formatValueFile3,
1671
+ formatValueFile as formatValueFile4,
1311
1672
  isSecret as isSecret3,
1312
- openValue as openValue2,
1673
+ openValue as openValue3,
1313
1674
  PenvError as PenvError7,
1314
1675
  parameterId as parameterId4,
1315
1676
  sealValue as sealValue2
@@ -1317,7 +1678,7 @@ import {
1317
1678
  import { defineCommand as defineCommand5 } from "citty";
1318
1679
 
1319
1680
  // src/commands/set.ts
1320
- import { formatValueFile as formatValueFile2, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
1681
+ import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
1321
1682
  import { defineCommand as defineCommand4 } from "citty";
1322
1683
  function scopeFrom(options) {
1323
1684
  if (options.local === true) {
@@ -1352,38 +1713,50 @@ function sealFor(project, file, value, parameter, environment) {
1352
1713
  if (environment === void 0) {
1353
1714
  throw new PenvError6(
1354
1715
  "SECRET_SCOPE_AMBIGUOUS",
1355
- `Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile2(file)} names no environment`,
1716
+ `Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
1356
1717
  "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
1718
  );
1358
1719
  }
1359
1720
  return sealValue(file, value, keySourceFor(project, environment), parameter, environment);
1360
1721
  }
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);
1722
+ async function sealAwareWrite(options) {
1723
+ const { project, provider, ref, scope, value, environment } = options;
1724
+ const secret = isSecret2(await provider.readMeta(ref), environment);
1367
1725
  const file = {
1368
1726
  namespace: ref.namespace,
1369
1727
  name: ref.name,
1370
1728
  scope,
1371
1729
  encrypted: secret
1372
1730
  };
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 };
1731
+ const stored = secret ? sealFor(project, file, value, parameterId3(ref), environment) : value;
1732
+ await provider.write(file, stored);
1733
+ await provider.remove({ ...file, encrypted: !secret });
1734
+ return { encrypted: secret, location: formatValueFile3(file) };
1377
1735
  }
1378
- function renderSet(result) {
1736
+ async function runSet(options) {
1737
+ const project = openProject(options.cwd);
1738
+ const ref = refFromKey(options.key, project.config);
1739
+ const scope = targetScope(project, options, options.key);
1740
+ const environment = policyEnvironment(project, options);
1741
+ const { encrypted, location } = await sealAwareWrite({
1742
+ project,
1743
+ provider: project.provider,
1744
+ ref,
1745
+ scope,
1746
+ value: options.value,
1747
+ environment
1748
+ });
1749
+ return { parameter: options.key, location, encrypted };
1750
+ }
1751
+ function renderSet(result2) {
1379
1752
  return formatRows([
1380
1753
  {
1381
1754
  glyph: CHECK,
1382
1755
  label: "Wrote",
1383
- subject: `${PENV_DIR}/${result.location}`,
1756
+ subject: `${PENV_DIR}/${result2.location}`,
1384
1757
  // The `.enc` suffix says this already, but only to a reader who knows the
1385
1758
  // grammar. Meta decided it, not the command line, so the command says so.
1386
- ...result.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1759
+ ...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
1387
1760
  }
1388
1761
  ]);
1389
1762
  }
@@ -1460,7 +1833,7 @@ async function runEncrypt(options) {
1460
1833
  const already = await readOne(project, sealed);
1461
1834
  throw new PenvError7(
1462
1835
  "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)}`,
1836
+ 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
1837
  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
1838
  );
1466
1839
  }
@@ -1469,8 +1842,8 @@ async function runEncrypt(options) {
1469
1842
  await project.provider.remove(plain);
1470
1843
  return {
1471
1844
  parameter,
1472
- location: formatValueFile3(sealed),
1473
- removed: formatValueFile3(plain)
1845
+ location: formatValueFile4(sealed),
1846
+ removed: formatValueFile4(plain)
1474
1847
  };
1475
1848
  }
1476
1849
  async function runDecrypt(options) {
@@ -1489,16 +1862,16 @@ async function runDecrypt(options) {
1489
1862
  if (stored === void 0) {
1490
1863
  throw new PenvError7(
1491
1864
  "PARAMETER_ABSENT",
1492
- `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile3(sealed)}`,
1865
+ `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
1493
1866
  `Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
1494
1867
  );
1495
1868
  }
1496
- const opened = openValue2(sealed, stored, keySourceFor(project, environment));
1869
+ const opened = openValue3(sealed, stored, keySourceFor(project, environment));
1497
1870
  if (opened.kind === "failed") {
1498
1871
  throw new UndecryptableAt(
1499
1872
  parameter,
1500
1873
  environment,
1501
- formatValueFile3(sealed),
1874
+ formatValueFile4(sealed),
1502
1875
  opened.failure.detail
1503
1876
  );
1504
1877
  }
@@ -1506,8 +1879,8 @@ async function runDecrypt(options) {
1506
1879
  await project.provider.remove(sealed);
1507
1880
  return {
1508
1881
  parameter,
1509
- location: formatValueFile3(plain),
1510
- removed: formatValueFile3(sealed)
1882
+ location: formatValueFile4(plain),
1883
+ removed: formatValueFile4(sealed)
1511
1884
  };
1512
1885
  }
1513
1886
  var UndecryptableAt = class extends PenvError7 {
@@ -1519,13 +1892,13 @@ var UndecryptableAt = class extends PenvError7 {
1519
1892
  );
1520
1893
  }
1521
1894
  };
1522
- function renderReseal(result, verb) {
1895
+ function renderReseal(result2, verb) {
1523
1896
  return formatRows([
1524
1897
  {
1525
1898
  glyph: CHECK,
1526
1899
  label: verb,
1527
- subject: `${PENV_DIR}/${result.location}`,
1528
- detail: `${PENV_DIR}/${result.removed} removed`
1900
+ subject: `${PENV_DIR}/${result2.location}`,
1901
+ detail: `${PENV_DIR}/${result2.removed} removed`
1529
1902
  }
1530
1903
  ]);
1531
1904
  }
@@ -1548,8 +1921,8 @@ var encryptCommand = defineCommand5({
1548
1921
  args: SCOPE_ARGS,
1549
1922
  run({ args }) {
1550
1923
  return guard(async () => {
1551
- const result = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1552
- write(renderReseal(result, "Encrypted"));
1924
+ const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1925
+ write(renderReseal(result2, "Encrypted"));
1553
1926
  });
1554
1927
  }
1555
1928
  });
@@ -1558,15 +1931,15 @@ var decryptCommand = defineCommand5({
1558
1931
  args: SCOPE_ARGS,
1559
1932
  run({ args }) {
1560
1933
  return guard(async () => {
1561
- const result = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1562
- write(renderReseal(result, "Decrypted"));
1934
+ const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
1935
+ write(renderReseal(result2, "Decrypted"));
1563
1936
  });
1564
1937
  }
1565
1938
  });
1566
1939
 
1567
1940
  // src/commands/generate.ts
1568
1941
  import { writeFileSync } from "fs";
1569
- import { isAbsolute, relative, resolve as resolve2 } from "path";
1942
+ import { isAbsolute, relative, resolve as resolve3 } from "path";
1570
1943
  import {
1571
1944
  checkNameCollisions as checkNameCollisions3,
1572
1945
  effectiveMeta as effectiveMeta2,
@@ -1625,7 +1998,7 @@ function runGenerate(options) {
1625
1998
  const project = openProject(options.cwd);
1626
1999
  const environment = targetEnvironment(project, options.environment);
1627
2000
  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);
2001
+ const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
1629
2002
  writeFileSync(file, serializeDotenv(entries), "utf8");
1630
2003
  return { file, environment, entries: entries.length, decrypted };
1631
2004
  }
@@ -1633,20 +2006,20 @@ function displayPath(cwd, file) {
1633
2006
  const rel = relative(cwd, file);
1634
2007
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
1635
2008
  }
1636
- function renderGenerate(result, cwd) {
2009
+ function renderGenerate(result2, cwd) {
1637
2010
  const rows = [
1638
2011
  {
1639
2012
  glyph: CHECK,
1640
2013
  label: "Generated",
1641
- subject: displayPath(cwd, result.file),
1642
- detail: `${result.entries} variables for environment ${result.environment}`
2014
+ subject: displayPath(cwd, result2.file),
2015
+ detail: `${result2.entries} variables for environment ${result2.environment}`
1643
2016
  }
1644
2017
  ];
1645
- if (result.decrypted > 0) {
2018
+ if (result2.decrypted > 0) {
1646
2019
  rows.push({
1647
2020
  glyph: WARN,
1648
2021
  label: "Decrypted",
1649
- subject: `${result.decrypted} ${result.decrypted === 1 ? "secret" : "secrets"}`,
2022
+ subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
1650
2023
  detail: "written as plaintext into the artifact"
1651
2024
  });
1652
2025
  }
@@ -1665,13 +2038,13 @@ var generateCommand = defineCommand6({
1665
2038
  run({ args }) {
1666
2039
  return guard(async () => {
1667
2040
  const cwd = process.cwd();
1668
- const result = runGenerate({
2041
+ const result2 = runGenerate({
1669
2042
  cwd,
1670
2043
  ...args.env === void 0 ? {} : { environment: args.env },
1671
2044
  ...args.out === void 0 ? {} : { out: args.out },
1672
2045
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1673
2046
  });
1674
- write(renderGenerate(result, cwd));
2047
+ write(renderGenerate(result2, cwd));
1675
2048
  });
1676
2049
  }
1677
2050
  });
@@ -1762,10 +2135,10 @@ var getCommand = defineCommand7({
1762
2135
 
1763
2136
  // src/commands/import.ts
1764
2137
  import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1765
- import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve4 } from "path";
2138
+ import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
1766
2139
  import {
1767
2140
  accessPath as accessPath4,
1768
- assertNever,
2141
+ assertNever as assertNever2,
1769
2142
  checkNameCollisions as checkNameCollisions4,
1770
2143
  FilenameGrammarError,
1771
2144
  findConfigFile,
@@ -1886,7 +2259,7 @@ function hasImportsBlock(cwd) {
1886
2259
 
1887
2260
  // src/commands/init.ts
1888
2261
  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";
2262
+ import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
1890
2263
  import { createInterface } from "readline/promises";
1891
2264
  import {
1892
2265
  DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
@@ -2356,7 +2729,7 @@ function renderTsconfig(target, alias) {
2356
2729
  `;
2357
2730
  }
2358
2731
  function ensurePenvDir(root) {
2359
- const dir = resolve3(root, PENV_DIR2);
2732
+ const dir = resolve4(root, PENV_DIR2);
2360
2733
  if (existsSync2(dir)) {
2361
2734
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
2362
2735
  }
@@ -2373,7 +2746,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2373
2746
  note: "(yours \u2014 penv never regenerates it)"
2374
2747
  };
2375
2748
  }
2376
- mkdirSync(dirname2(file), { recursive: true });
2749
+ mkdirSync(dirname3(file), { recursive: true });
2377
2750
  writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
2378
2751
  return {
2379
2752
  target: "schema",
@@ -2461,20 +2834,20 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
2461
2834
  ];
2462
2835
  }
2463
2836
  function runInit(options) {
2464
- const root = resolve3(options.cwd);
2837
+ const root = resolve4(options.cwd);
2465
2838
  const decisions = options.decisions ?? planInit(root).decisions;
2466
2839
  return { root, decisions, steps: scaffold(root, [], false, decisions) };
2467
2840
  }
2468
- function renderInit(result) {
2469
- const steps = result.steps.map((step) => {
2841
+ function renderInit(result2) {
2842
+ const steps = result2.steps.map((step) => {
2470
2843
  const glyph = step.action === "conflicted" ? WARN : CHECK;
2471
2844
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
2472
2845
  });
2473
2846
  return [
2474
2847
  ...formatSteps(steps),
2475
2848
  "",
2476
- `Done. Declare your parameters in ${result.decisions.schemaFile}, then \`penv set <key>\`.`,
2477
- ...result.decisions.environments.length === 0 ? [
2849
+ `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
2850
+ ...result2.decisions.environments.length === 0 ? [
2478
2851
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2479
2852
  ] : []
2480
2853
  ];
@@ -2513,7 +2886,7 @@ var initCommand = defineCommand8({
2513
2886
  },
2514
2887
  run({ args }) {
2515
2888
  return guard(async () => {
2516
- const root = resolve3(process.cwd());
2889
+ const root = resolve4(process.cwd());
2517
2890
  const environments = environmentsFromFlag(args.env);
2518
2891
  const plan2 = planInit(root, {
2519
2892
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -2647,7 +3020,7 @@ function environmentOf(scope) {
2647
3020
  case "local":
2648
3021
  return void 0;
2649
3022
  default:
2650
- return assertNever(scope, "scope");
3023
+ return assertNever2(scope, "scope");
2651
3024
  }
2652
3025
  }
2653
3026
  function scopeWithEnvironment(scope, environment) {
@@ -2660,7 +3033,7 @@ function scopeWithEnvironment(scope, environment) {
2660
3033
  case "environment-local":
2661
3034
  return scope;
2662
3035
  default:
2663
- return assertNever(scope, "scope");
3036
+ return assertNever2(scope, "scope");
2664
3037
  }
2665
3038
  }
2666
3039
  function explicitEnvironment(options, source, config) {
@@ -2719,8 +3092,8 @@ function configInEffect(cwd, environment) {
2719
3092
  return { config: openProject(cwd).config, decisions };
2720
3093
  }
2721
3094
  function importDotenv(options) {
2722
- const cwd = resolve4(options.cwd);
2723
- const file = isAbsolute2(options.file) ? options.file : resolve4(cwd, options.file);
3095
+ const cwd = resolve5(options.cwd);
3096
+ const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
2724
3097
  if (!existsSync3(file)) {
2725
3098
  throw new PenvError11(
2726
3099
  "IMPORT_FILE_MISSING",
@@ -2803,19 +3176,19 @@ function skippedValidationStep(environments) {
2803
3176
  note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
2804
3177
  };
2805
3178
  }
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";
3179
+ function renderImport(result2, validation) {
3180
+ const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
3181
+ if (result2.orphanComments > 0) {
3182
+ const plural = result2.orphanComments === 1 ? "comment" : "comments";
2810
3183
  steps.push({
2811
3184
  glyph: WARN,
2812
- text: `Dropped ${result.orphanComments} orphan ${plural}`,
3185
+ text: `Dropped ${result2.orphanComments} orphan ${plural}`,
2813
3186
  note: "attached to no variable, so nothing to describe"
2814
3187
  });
2815
3188
  }
2816
- for (const step of result.steps) {
3189
+ for (const step of result2.steps) {
2817
3190
  if (step.target === "schema" && step.action === "kept") {
2818
- steps.push(keptSchemaStep(step, result.variables));
3191
+ steps.push(keptSchemaStep(step, result2.variables));
2819
3192
  continue;
2820
3193
  }
2821
3194
  const glyph = step.action === "conflicted" ? WARN : CHECK;
@@ -2823,10 +3196,10 @@ function renderImport(result, validation) {
2823
3196
  step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
2824
3197
  );
2825
3198
  }
2826
- steps.push({ glyph: CHECK, text: `Created ${displayPath2(result.root, result.backup)}` });
3199
+ steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
2827
3200
  const lines = formatSteps(steps);
2828
3201
  if (validation === void 0) {
2829
- lines.push(...formatSteps([skippedValidationStep(result.environments)]));
3202
+ lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
2830
3203
  } else if (validation.ok) {
2831
3204
  lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
2832
3205
  } else {
@@ -2871,11 +3244,11 @@ import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError12 } from "@penvhq/c
2871
3244
  import { defineCommand as defineCommand10 } from "citty";
2872
3245
 
2873
3246
  // src/keychain.ts
2874
- import { createRequire } from "module";
3247
+ import { createRequire as createRequire2 } from "module";
2875
3248
  var cached;
2876
3249
  function entryConstructor() {
2877
3250
  if (cached === void 0) {
2878
- const require2 = createRequire(import.meta.url);
3251
+ const require2 = createRequire2(import.meta.url);
2879
3252
  cached = require2("@napi-rs/keyring").Entry;
2880
3253
  }
2881
3254
  return cached;
@@ -2939,19 +3312,19 @@ function runKeyCreate(options) {
2939
3312
  key
2940
3313
  };
2941
3314
  }
2942
- function renderKeyCreate(result) {
2943
- if (result.source === "keychain") {
3315
+ function renderKeyCreate(result2) {
3316
+ if (result2.source === "keychain") {
2944
3317
  return [
2945
- `A new key for environment ${result.environment}, stored in your OS keychain as \`${result.id}\`.`,
3318
+ `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
2946
3319
  "",
2947
3320
  "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
2948
3321
  "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
2949
3322
  ];
2950
3323
  }
2951
3324
  return [
2952
- `A new key for environment ${result.environment}. penv did not store it.`,
3325
+ `A new key for environment ${result2.environment}. penv did not store it.`,
2953
3326
  "",
2954
- ` ${result.variable}=${result.key}`,
3327
+ ` ${result2.variable}=${result2.key}`,
2955
3328
  "",
2956
3329
  "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
2957
3330
  "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
@@ -2988,7 +3361,7 @@ var keyCommand = defineCommand10({
2988
3361
  });
2989
3362
 
2990
3363
  // src/commands/list.ts
2991
- import { assertNever as assertNever2, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
3364
+ import { assertNever as assertNever3, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
2992
3365
  import { defineCommand as defineCommand11 } from "citty";
2993
3366
  function scopeLabel2(scope) {
2994
3367
  switch (scope.kind) {
@@ -3001,7 +3374,7 @@ function scopeLabel2(scope) {
3001
3374
  case "unscoped":
3002
3375
  return "default";
3003
3376
  default:
3004
- return assertNever2(scope, "scope");
3377
+ return assertNever3(scope, "scope");
3005
3378
  }
3006
3379
  }
3007
3380
  async function runList(options) {
@@ -3023,11 +3396,11 @@ async function runList(options) {
3023
3396
  }
3024
3397
  return { environment, parameters };
3025
3398
  }
3026
- function renderList(result) {
3027
- if (result.parameters.length === 0) {
3028
- return [`No parameters in ${PENV_DIR}/ for environment ${result.environment}.`];
3399
+ function renderList(result2) {
3400
+ if (result2.parameters.length === 0) {
3401
+ return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3029
3402
  }
3030
- return columns(result.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3403
+ return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3031
3404
  }
3032
3405
  var listCommand = defineCommand11({
3033
3406
  meta: { name: "list", description: "List parameters" },
@@ -3037,11 +3410,11 @@ var listCommand = defineCommand11({
3037
3410
  },
3038
3411
  run({ args }) {
3039
3412
  return guard(async () => {
3040
- const result = await runList({
3413
+ const result2 = await runList({
3041
3414
  cwd: process.cwd(),
3042
3415
  ...args.env === void 0 ? {} : { environment: args.env }
3043
3416
  });
3044
- write(args.json === true ? [JSON.stringify(result, null, 2)] : renderList(result));
3417
+ write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
3045
3418
  });
3046
3419
  }
3047
3420
  });
@@ -3050,8 +3423,8 @@ var listCommand = defineCommand11({
3050
3423
  import {
3051
3424
  accessPath as accessPath5,
3052
3425
  formatMetaFile,
3053
- formatValueFile as formatValueFile4,
3054
- openValue as openValue3,
3426
+ formatValueFile as formatValueFile5,
3427
+ openValue as openValue4,
3055
3428
  PenvError as PenvError13,
3056
3429
  parameterId as parameterId5,
3057
3430
  sealValue as sealValue3
@@ -3073,16 +3446,16 @@ async function planFile(project, source, target, parameter) {
3073
3446
  if (environment === void 0) {
3074
3447
  throw new PenvError13(
3075
3448
  "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`,
3449
+ `${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3077
3450
  "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
3451
  );
3079
3452
  }
3080
3453
  const keys = keySourceFor(project, environment);
3081
- const opened = openValue3(source, stored, keys);
3454
+ const opened = openValue4(source, stored, keys);
3082
3455
  if (opened.kind === "failed") {
3083
3456
  throw new PenvError13(
3084
3457
  "VALUE_UNDECRYPTABLE",
3085
- `${PENV_DIR}/${formatValueFile4(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3458
+ `${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3086
3459
  "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
3460
  );
3088
3461
  }
@@ -3150,8 +3523,8 @@ async function runMove(options) {
3150
3523
  from: parameterId5(from),
3151
3524
  to: parameterId5(to),
3152
3525
  files: planned.map((file) => ({
3153
- from: formatValueFile4(file.source),
3154
- to: formatValueFile4(file.target),
3526
+ from: formatValueFile5(file.source),
3527
+ to: formatValueFile5(file.target),
3155
3528
  resealed: file.resealed
3156
3529
  })),
3157
3530
  // The meta's path, not the parameter's dotted id: `redis.password` is what
@@ -3161,20 +3534,20 @@ async function runMove(options) {
3161
3534
  schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
3162
3535
  };
3163
3536
  }
3164
- function renderMove(result) {
3165
- const rows = result.files.map((file) => ({
3537
+ function renderMove(result2) {
3538
+ const rows = result2.files.map((file) => ({
3166
3539
  glyph: CHECK,
3167
3540
  label: "Moved",
3168
3541
  subject: `${PENV_DIR}/${file.to}`,
3169
3542
  ...file.resealed ? { detail: "re-sealed for its new address" } : {}
3170
3543
  }));
3171
- if (result.meta !== void 0) {
3172
- rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result.meta}` });
3544
+ if (result2.meta !== void 0) {
3545
+ rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
3173
3546
  }
3174
3547
  const lines = formatRows(rows);
3175
3548
  lines.push(
3176
3549
  "",
3177
- ` .penv/env.ts still declares \`${result.schema.was}\`. Rename it to \`${result.schema.now}\`,`,
3550
+ ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3178
3551
  " or `penv validate` will report the value as unused and the declaration as unset."
3179
3552
  );
3180
3553
  return lines;
@@ -3200,9 +3573,86 @@ var mvCommand = defineCommand12({
3200
3573
  }
3201
3574
  });
3202
3575
 
3203
- // src/commands/remove.ts
3204
- import { formatValueFile as formatValueFile5 } from "@penvhq/core";
3576
+ // src/commands/pull.ts
3205
3577
  import { defineCommand as defineCommand13 } from "citty";
3578
+ async function runPull(options) {
3579
+ const project = openProject(options.cwd);
3580
+ const environment = targetEnvironment(project, options.environment);
3581
+ const source = await sourceProviderFor(project, environment);
3582
+ if (source.type === LOCAL_TREE_TYPE) {
3583
+ return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
3584
+ }
3585
+ const tree = localTree(project);
3586
+ const files = await source.list();
3587
+ let values = 0;
3588
+ for (const file of files) {
3589
+ const value = await source.read(file);
3590
+ if (value === void 0) {
3591
+ continue;
3592
+ }
3593
+ tree.writeSync(file, value);
3594
+ values += 1;
3595
+ }
3596
+ const refs = refsFrom(files);
3597
+ let meta = 0;
3598
+ for (const ref of refs) {
3599
+ const block = await source.readMeta(ref);
3600
+ if (block === void 0) {
3601
+ continue;
3602
+ }
3603
+ tree.writeMetaSync(ref, block);
3604
+ meta += 1;
3605
+ }
3606
+ return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
3607
+ }
3608
+ function renderPull(result2) {
3609
+ if (result2.localSource) {
3610
+ return formatRows([
3611
+ {
3612
+ glyph: CHECK,
3613
+ label: "Nothing to pull",
3614
+ subject: `environment ${result2.environment} has no separate source of truth`,
3615
+ detail: "its values live in the local .penv tree already"
3616
+ }
3617
+ ]);
3618
+ }
3619
+ return formatRows([
3620
+ {
3621
+ glyph: CHECK,
3622
+ label: "Pulled",
3623
+ subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
3624
+ detail: `from the ${result2.source} provider for environment ${result2.environment}`
3625
+ },
3626
+ {
3627
+ glyph: CHECK,
3628
+ label: "Parameters",
3629
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
3630
+ detail: "written into the local .penv tree"
3631
+ }
3632
+ ]);
3633
+ }
3634
+ var pullCommand = defineCommand13({
3635
+ meta: {
3636
+ name: "pull",
3637
+ description: "Materialise the local .penv tree from an environment's source-of-truth provider"
3638
+ },
3639
+ args: {
3640
+ env: { type: "string", description: "The environment to pull" }
3641
+ },
3642
+ run({ args }) {
3643
+ return guard(async () => {
3644
+ const result2 = await runPull({
3645
+ cwd: process.cwd(),
3646
+ ...args.env === void 0 ? {} : { environment: args.env }
3647
+ });
3648
+ write(renderPull(result2));
3649
+ });
3650
+ }
3651
+ });
3652
+
3653
+ // src/commands/remove.ts
3654
+ import { formatValueFile as formatValueFile6 } from "@penvhq/core";
3655
+ import { defineCommand as defineCommand14 } from "citty";
3206
3656
  async function runRemove(options) {
3207
3657
  const project = openProject(options.cwd);
3208
3658
  const ref = refFromKey(options.key);
@@ -3219,17 +3669,17 @@ async function runRemove(options) {
3219
3669
  continue;
3220
3670
  }
3221
3671
  await project.provider.remove(file);
3222
- removed.push(formatValueFile5(file));
3672
+ removed.push(formatValueFile6(file));
3223
3673
  }
3224
3674
  return {
3225
3675
  parameter: options.key,
3226
3676
  removed,
3227
- considered: files.map((file) => formatValueFile5(file))
3677
+ considered: files.map((file) => formatValueFile6(file))
3228
3678
  };
3229
3679
  }
3230
- function renderRemove(result) {
3231
- if (result.removed.length === 0) {
3232
- const first = result.considered[0] ?? result.parameter;
3680
+ function renderRemove(result2) {
3681
+ if (result2.removed.length === 0) {
3682
+ const first = result2.considered[0] ?? result2.parameter;
3233
3683
  return formatRows([
3234
3684
  {
3235
3685
  glyph: WARN,
@@ -3239,14 +3689,14 @@ function renderRemove(result) {
3239
3689
  }
3240
3690
  ]);
3241
3691
  }
3242
- const rows = result.removed.map((location) => ({
3692
+ const rows = result2.removed.map((location) => ({
3243
3693
  glyph: CHECK,
3244
3694
  label: "Removed",
3245
3695
  subject: `${PENV_DIR}/${location}`
3246
3696
  }));
3247
3697
  return formatRows(rows);
3248
3698
  }
3249
- var removeCommand = defineCommand13({
3699
+ var removeCommand = defineCommand14({
3250
3700
  meta: { name: "remove", description: "Delete a parameter" },
3251
3701
  args: {
3252
3702
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -3272,11 +3722,197 @@ var removeCommand = defineCommand13({
3272
3722
  }
3273
3723
  });
3274
3724
 
3725
+ // src/commands/rotate.ts
3726
+ import {
3727
+ beginRotation,
3728
+ completeRotation,
3729
+ PenvError as PenvError14,
3730
+ retainsPrevious,
3731
+ rotationOf as rotationOf2
3732
+ } from "@penvhq/core";
3733
+ import { defineCommand as defineCommand15 } from "citty";
3734
+ function rotatingFile(ref, environment) {
3735
+ return {
3736
+ namespace: ref.namespace,
3737
+ name: ref.name,
3738
+ scope: { kind: "environment", environment },
3739
+ encrypted: false
3740
+ };
3741
+ }
3742
+ async function writeRotatedValue(project, provider, ref, environment, value) {
3743
+ if (provider.type === LOCAL_TREE_TYPE) {
3744
+ await sealAwareWrite({
3745
+ project,
3746
+ provider,
3747
+ ref,
3748
+ scope: { kind: "environment", environment },
3749
+ value,
3750
+ environment
3751
+ });
3752
+ return;
3753
+ }
3754
+ await provider.write(rotatingFile(ref, environment), value);
3755
+ }
3756
+ function requireNewValue(value, phase, key) {
3757
+ if (value === void 0) {
3758
+ throw new PenvError14(
3759
+ "ROTATION_NO_VALUE",
3760
+ `A ${phase} rotation of ${key} writes a new value, and none was given`,
3761
+ "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
3762
+ );
3763
+ }
3764
+ return value;
3765
+ }
3766
+ function requireRetaining(provider, environment) {
3767
+ if (!retainsPrevious(provider)) {
3768
+ throw new PenvError14(
3769
+ "ROTATION_NOT_RETAINING",
3770
+ `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`,
3771
+ "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."
3772
+ );
3773
+ }
3774
+ return provider;
3775
+ }
3776
+ async function runRotate(options) {
3777
+ const project = openProject(options.cwd);
3778
+ const environment = targetEnvironment(project, options.environment);
3779
+ const ref = refFromKey(options.key, project.config);
3780
+ const provider = await sourceProviderFor(project, environment);
3781
+ const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
3782
+ const before = await provider.readMeta(ref);
3783
+ const { mechanism } = rotationOf2(before, environment);
3784
+ if (mechanism === void 0) {
3785
+ throw new PenvError14(
3786
+ "ROTATION_NO_MECHANISM",
3787
+ `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
3788
+ '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.'
3789
+ );
3790
+ }
3791
+ const begin = options.begin === true;
3792
+ const complete = options.complete === true;
3793
+ if (mechanism === "atomic-cutover") {
3794
+ if (begin || complete) {
3795
+ throw new PenvError14(
3796
+ "ROTATION_MECHANISM_MISMATCH",
3797
+ `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
3798
+ "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."
3799
+ );
3800
+ }
3801
+ const value = requireNewValue(options.value, "cutover", options.key);
3802
+ await writeRotatedValue(project, provider, ref, environment, value);
3803
+ const after2 = completeRotation(before, environment, nowIso);
3804
+ await provider.writeMeta(ref, after2);
3805
+ return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
3806
+ }
3807
+ const retaining = requireRetaining(provider, environment);
3808
+ if (begin === complete) {
3809
+ throw new PenvError14(
3810
+ "ROTATION_PHASE_REQUIRED",
3811
+ `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
3812
+ "`--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."
3813
+ );
3814
+ }
3815
+ if (begin) {
3816
+ const value = requireNewValue(options.value, "begin", options.key);
3817
+ await writeRotatedValue(project, retaining, ref, environment, value);
3818
+ const after2 = beginRotation(before, environment, nowIso);
3819
+ await retaining.writeMeta(ref, after2);
3820
+ return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
3821
+ }
3822
+ const after = completeRotation(before, environment, nowIso);
3823
+ await retaining.writeMeta(ref, after);
3824
+ return result(ref, environment, mechanism, "complete", retaining.type, false, after);
3825
+ }
3826
+ function result(ref, environment, mechanism, phase, source, wroteValue, after) {
3827
+ const { state, rotatingSince, lastRotated } = rotationOf2(after, environment);
3828
+ return {
3829
+ parameter: [...ref.namespace, ref.name].join("/"),
3830
+ environment,
3831
+ mechanism,
3832
+ phase,
3833
+ source,
3834
+ wroteValue,
3835
+ state,
3836
+ rotatingSince,
3837
+ lastRotated
3838
+ };
3839
+ }
3840
+ function renderRotate(result2) {
3841
+ if (result2.phase === "begin") {
3842
+ return formatRows([
3843
+ {
3844
+ glyph: CHECK,
3845
+ label: "Rotating",
3846
+ subject: result2.parameter,
3847
+ detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
3848
+ },
3849
+ {
3850
+ glyph: CHECK,
3851
+ label: "Previous",
3852
+ subject: "still readable",
3853
+ detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
3854
+ }
3855
+ ]);
3856
+ }
3857
+ if (result2.phase === "complete") {
3858
+ return formatRows([
3859
+ {
3860
+ glyph: CHECK,
3861
+ label: "Rotated",
3862
+ subject: result2.parameter,
3863
+ detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
3864
+ }
3865
+ ]);
3866
+ }
3867
+ return formatRows([
3868
+ {
3869
+ glyph: CHECK,
3870
+ label: "Rotated",
3871
+ subject: result2.parameter,
3872
+ detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
3873
+ }
3874
+ ]);
3875
+ }
3876
+ var rotateCommand = defineCommand15({
3877
+ meta: {
3878
+ name: "rotate",
3879
+ description: "Rotate a parameter's value by the mechanism its meta declares"
3880
+ },
3881
+ args: {
3882
+ key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
3883
+ value: {
3884
+ type: "positional",
3885
+ required: false,
3886
+ description: "The new value; read from stdin if omitted. Not needed with --complete"
3887
+ },
3888
+ env: { type: "string", description: "The environment to rotate in" },
3889
+ begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
3890
+ complete: { type: "boolean", description: "Close a dual-valid grace window" }
3891
+ },
3892
+ run({ args }) {
3893
+ return guard(async () => {
3894
+ const value = args.complete === true ? void 0 : args.value ?? await readStdin();
3895
+ write(
3896
+ renderRotate(
3897
+ await runRotate({
3898
+ cwd: process.cwd(),
3899
+ key: args.key,
3900
+ ...value === void 0 ? {} : { value },
3901
+ ...args.env === void 0 ? {} : { environment: args.env },
3902
+ ...args.begin === void 0 ? {} : { begin: args.begin },
3903
+ ...args.complete === void 0 ? {} : { complete: args.complete }
3904
+ })
3905
+ )
3906
+ );
3907
+ });
3908
+ }
3909
+ });
3910
+
3275
3911
  // src/commands/watch.ts
3276
3912
  import { existsSync as existsSync4, watch } from "fs";
3277
- import { basename as basename2, dirname as dirname3, resolve as resolve5 } from "path";
3913
+ import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
3278
3914
  import { schemaFileOf as schemaFileOf4, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
3279
- import { defineCommand as defineCommand14 } from "citty";
3915
+ import { defineCommand as defineCommand16 } from "citty";
3280
3916
  var DEBOUNCE_MS = 100;
3281
3917
  function runWatch(options) {
3282
3918
  const project = openProject(options.cwd);
@@ -3297,12 +3933,12 @@ function runWatch(options) {
3297
3933
  }
3298
3934
  running = true;
3299
3935
  try {
3300
- const result = await runValidate({
3936
+ const result2 = await runValidate({
3301
3937
  cwd: options.cwd,
3302
3938
  ...options.environment === void 0 ? {} : { environment: options.environment }
3303
3939
  });
3304
3940
  if (!closed) {
3305
- options.onResult?.(result);
3941
+ options.onResult?.(result2);
3306
3942
  }
3307
3943
  } catch (error) {
3308
3944
  if (!closed) {
@@ -3333,7 +3969,7 @@ function runWatch(options) {
3333
3969
  watcher.close();
3334
3970
  }
3335
3971
  function armRecovery(target, recursive, only) {
3336
- const parent = dirname3(target);
3972
+ const parent = dirname4(target);
3337
3973
  const name = basename2(target);
3338
3974
  let recovery;
3339
3975
  try {
@@ -3407,10 +4043,10 @@ function runWatch(options) {
3407
4043
  watchers.add(watcher);
3408
4044
  }
3409
4045
  addWatcher(project.penvDir, true);
3410
- addWatcher(dirname3(project.configFile), false, configFile);
4046
+ addWatcher(dirname4(project.configFile), false, configFile);
3411
4047
  if (schemaInsideTree2(project.config) === void 0) {
3412
- const schemaFile = resolve5(project.root, schemaFileOf4(project.config));
3413
- addWatcher(dirname3(schemaFile), false, basename2(schemaFile));
4048
+ const schemaFile = resolve6(project.root, schemaFileOf4(project.config));
4049
+ addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
3414
4050
  }
3415
4051
  void validate();
3416
4052
  return {
@@ -3426,8 +4062,8 @@ function runWatch(options) {
3426
4062
  }
3427
4063
  };
3428
4064
  }
3429
- function renderWatch(result) {
3430
- return ["", ...renderValidate(result), ...renderDrift(result.drift, result.environment)];
4065
+ function renderWatch(result2) {
4066
+ return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
3431
4067
  }
3432
4068
  function renderDrift(drift, environment) {
3433
4069
  const rows = [
@@ -3453,7 +4089,7 @@ function renderDrift(drift, environment) {
3453
4089
  }
3454
4090
  return lines;
3455
4091
  }
3456
- var watchCommand = defineCommand14({
4092
+ var watchCommand = defineCommand16({
3457
4093
  meta: {
3458
4094
  name: "watch",
3459
4095
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -3466,8 +4102,8 @@ var watchCommand = defineCommand14({
3466
4102
  const handle = runWatch({
3467
4103
  cwd: process.cwd(),
3468
4104
  ...args.env === void 0 ? {} : { environment: args.env },
3469
- onResult: (result) => {
3470
- write(renderWatch(result));
4105
+ onResult: (result2) => {
4106
+ write(renderWatch(result2));
3471
4107
  },
3472
4108
  // A failing cycle is reported and watching continues. `reportError`
3473
4109
  // marks the process failed, which `validate` wants and a loop does not:
@@ -3481,10 +4117,10 @@ var watchCommand = defineCommand14({
3481
4117
  }
3482
4118
  });
3483
4119
  write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
3484
- await new Promise((resolve6) => {
4120
+ await new Promise((resolve7) => {
3485
4121
  process.once("SIGINT", () => {
3486
4122
  handle.close();
3487
- resolve6();
4123
+ resolve7();
3488
4124
  });
3489
4125
  });
3490
4126
  });
@@ -3492,7 +4128,7 @@ var watchCommand = defineCommand14({
3492
4128
  });
3493
4129
 
3494
4130
  // src/index.ts
3495
- var main = defineCommand15({
4131
+ var main = defineCommand17({
3496
4132
  meta: {
3497
4133
  name: "penv",
3498
4134
  description: "Configuration that shares a data model with your production secret manager"
@@ -3504,7 +4140,9 @@ var main = defineCommand15({
3504
4140
  get: getCommand,
3505
4141
  set: setCommand,
3506
4142
  mv: mvCommand,
4143
+ pull: pullCommand,
3507
4144
  push: pushCommand,
4145
+ rotate: rotateCommand,
3508
4146
  remove: removeCommand,
3509
4147
  list: listCommand,
3510
4148
  encrypt: encryptCommand,
@@ -3527,7 +4165,9 @@ export {
3527
4165
  main,
3528
4166
  renderDoctor,
3529
4167
  renderMove,
4168
+ renderPull,
3530
4169
  renderPush,
4170
+ renderRotate,
3531
4171
  renderWatch,
3532
4172
  runDecrypt,
3533
4173
  runDoctor,
@@ -3539,8 +4179,10 @@ export {
3539
4179
  runList,
3540
4180
  runMain,
3541
4181
  runMove,
4182
+ runPull,
3542
4183
  runPush,
3543
4184
  runRemove,
4185
+ runRotate,
3544
4186
  runSet,
3545
4187
  runValidate,
3546
4188
  runWatch