@penvhq/cli 0.7.0 → 0.8.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
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  renderPull: () => renderPull,
32
32
  renderPush: () => renderPush,
33
33
  renderRotate: () => renderRotate,
34
+ renderSnapshot: () => renderSnapshot,
34
35
  renderWatch: () => renderWatch,
35
36
  runDecrypt: () => runDecrypt,
36
37
  runDoctor: () => runDoctor,
@@ -48,15 +49,18 @@ __export(index_exports, {
48
49
  runRemove: () => runRemove,
49
50
  runRotate: () => runRotate,
50
51
  runSet: () => runSet,
52
+ runSnapshot: () => runSnapshot,
51
53
  runValidate: () => runValidate,
52
54
  runWatch: () => runWatch
53
55
  });
54
56
  module.exports = __toCommonJS(index_exports);
55
- var import_core24 = require("@penvhq/core");
56
- var import_citty18 = require("citty");
57
+ var import_core25 = require("@penvhq/core");
58
+ var import_citty19 = require("citty");
57
59
 
58
60
  // src/commands/doctor.ts
59
- var import_core8 = require("@penvhq/core");
61
+ var import_node_fs3 = require("fs");
62
+ var import_node_path5 = require("path");
63
+ var import_core9 = require("@penvhq/core");
60
64
  var import_citty3 = require("citty");
61
65
 
62
66
  // src/env-flags.ts
@@ -118,6 +122,7 @@ function environmentFromShorthand(config, candidates, explicit) {
118
122
  }
119
123
 
120
124
  // src/project.ts
125
+ var import_node_fs = require("fs");
121
126
  var import_node_path2 = require("path");
122
127
  var import_core3 = require("@penvhq/core");
123
128
  var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
@@ -272,6 +277,18 @@ function localTree(project) {
272
277
  }
273
278
  return project.provider;
274
279
  }
280
+ function selfContainedSchemaModule(root, schemaFile) {
281
+ const file = (0, import_node_path2.join)(root, ...schemaFile.split("/"));
282
+ if (!(0, import_node_fs.existsSync)(file)) {
283
+ return void 0;
284
+ }
285
+ const source = (0, import_node_fs.readFileSync)(file, "utf8");
286
+ return source.includes("PenvSchemaShape") || source.includes("z.object") ? schemaFile : void 0;
287
+ }
288
+ function schemaShapeFileOf(project) {
289
+ const schemaFile = (0, import_core3.schemaFileOf)(project.config);
290
+ return selfContainedSchemaModule(project.root, schemaFile) ?? import_core3.SCHEMA_SHAPE_FILE;
291
+ }
275
292
  async function sourceProviderFor(project, environment) {
276
293
  const providerConfig = project.config.providers[environment];
277
294
  if (providerConfig === void 0) {
@@ -497,7 +514,7 @@ function hasValue(resolution) {
497
514
  return resolution.winner !== void 0;
498
515
  }
499
516
  function computeDrift(input) {
500
- const { schema, resolutions, config, environment } = input;
517
+ const { schema, resolutions, config, environment, schemaShapeFile } = input;
501
518
  const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
502
519
  const declared = [];
503
520
  const optional = [];
@@ -507,7 +524,7 @@ function computeDrift(input) {
507
524
  }
508
525
  const path = leaf.path.join(".");
509
526
  const ref = (0, import_core4.refFromAccessPath)(leaf.path);
510
- const renameRemedy = `Rename the \`${path}\` key in .penv/env.ts \u2014 a parameter name is lower-case, hyphenated, and never a reserved token, so no value file reaches this key.`;
527
+ const renameRemedy = `Rename the \`${path}\` key in ${schemaShapeFile} \u2014 a parameter name is lower-case, hyphenated, and never a reserved token, so no value file reaches this key.`;
511
528
  if (ref === void 0 || (0, import_core4.isReservedToken)(ref.name, config)) {
512
529
  if (leaf.absencePermitted) {
513
530
  optional.push({ subject: path, remedy: renameRemedy });
@@ -537,7 +554,7 @@ function computeDrift(input) {
537
554
  subject: (0, import_core4.parameterId)(ref),
538
555
  ref,
539
556
  remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`,
540
- detail: `declared in .penv/env.ts, no value for ${environment}`
557
+ detail: `declared in ${schemaShapeFile}, no value for ${environment}`
541
558
  });
542
559
  }
543
560
  const undeclared = [];
@@ -553,6 +570,133 @@ function computeDrift(input) {
553
570
  return { declared, undeclared, optional };
554
571
  }
555
572
 
573
+ // src/snapshot.ts
574
+ var import_node_fs2 = require("fs");
575
+ var import_node_path3 = require("path");
576
+ var import_core5 = require("@penvhq/core");
577
+ var import_provider_filesystem3 = require("@penvhq/provider-filesystem");
578
+ var SNAPSHOT_FILE = "penv.snapshot.ts";
579
+ function sealedValues(tree) {
580
+ const collected = {};
581
+ for (const file of tree.listSync()) {
582
+ if (!file.encrypted) {
583
+ continue;
584
+ }
585
+ if (file.scope.kind === "local" || file.scope.kind === "environment-local") {
586
+ continue;
587
+ }
588
+ const stored = tree.readSync(file);
589
+ if (stored === void 0) {
590
+ continue;
591
+ }
592
+ collected[(0, import_core5.formatValueFile)(file)] = stored;
593
+ }
594
+ const values = {};
595
+ for (const key of Object.keys(collected).sort()) {
596
+ values[key] = collected[key];
597
+ }
598
+ return values;
599
+ }
600
+ function buildSnapshotFrom(root, config) {
601
+ const tree = (0, import_provider_filesystem3.createFilesystemProvider)({ root: (0, import_node_path3.join)(root, PENV_DIR), config });
602
+ return { v: 1, config, values: sealedValues(tree) };
603
+ }
604
+ function buildSnapshot(project) {
605
+ return { v: 1, config: project.config, values: sealedValues(localTree(project)) };
606
+ }
607
+ function renderSnapshotModule(snapshot) {
608
+ const body = JSON.stringify(snapshot, null, 2);
609
+ return `// penv.snapshot.ts \u2014 GENERATED by penv. Do not edit; run \`penv snapshot\` to refresh.
610
+ // It embeds the evaluated config and every committed sealed (.enc) value, so \`load()\`
611
+ // resolves in a bundled or serverless runtime where no penv.config.ts is on disk.
612
+ import type { PenvSnapshot } from "@penvhq/penv";
613
+
614
+ export const snapshot = ${body} satisfies PenvSnapshot;
615
+ `;
616
+ }
617
+ function equalsIgnoringEol(a, b) {
618
+ return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
619
+ }
620
+ function writeSnapshotFile(project) {
621
+ const path = (0, import_node_path3.join)(project.root, SNAPSHOT_FILE);
622
+ const wanted = renderSnapshotModule(buildSnapshot(project));
623
+ const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : void 0;
624
+ if (existing !== void 0 && equalsIgnoringEol(existing, wanted)) {
625
+ return { file: SNAPSHOT_FILE, action: "unchanged" };
626
+ }
627
+ (0, import_node_fs2.writeFileSync)(path, wanted, "utf8");
628
+ return { file: SNAPSHOT_FILE, action: existing === void 0 ? "created" : "updated" };
629
+ }
630
+ function snapshotExists(root) {
631
+ return (0, import_node_fs2.existsSync)((0, import_node_path3.join)(root, SNAPSHOT_FILE));
632
+ }
633
+ function refreshSnapshot(project) {
634
+ if (snapshotExists(project.root)) {
635
+ writeSnapshotFile(project);
636
+ }
637
+ }
638
+ function snapshotSpecifier(schemaFile) {
639
+ const depth = schemaFile.split("/").length - 1;
640
+ const climb = depth === 0 ? "./" : "../".repeat(depth);
641
+ return `${climb}penv.snapshot.js`;
642
+ }
643
+ function insertSnapshotImport(source, importLine) {
644
+ const afterSchema = /^import\s*\{\s*schema\s*\}\s*from\s*["'][^"']*penv\.schema\.js["'];?[ \t]*\r?\n/m;
645
+ const schemaMatch = afterSchema.exec(source);
646
+ if (schemaMatch !== null) {
647
+ const index = schemaMatch.index + schemaMatch[0].length;
648
+ return `${source.slice(0, index)}${importLine}
649
+ ${source.slice(index)}`;
650
+ }
651
+ const afterLoad = /^import\s*\{[^}]*\}\s*from\s*["']@penvhq\/penv["'];?[ \t]*\r?\n/m;
652
+ const loadMatch = afterLoad.exec(source);
653
+ if (loadMatch !== null) {
654
+ const index = loadMatch.index + loadMatch[0].length;
655
+ return `${source.slice(0, index)}${importLine}
656
+ ${source.slice(index)}`;
657
+ }
658
+ return `${importLine}
659
+ ${source}`;
660
+ }
661
+ function wireEnvModule(project) {
662
+ const schemaFile = (0, import_core5.schemaFileOf)(project.config);
663
+ const specifier = snapshotSpecifier(schemaFile);
664
+ const importLine = `import { snapshot } from "${specifier}";`;
665
+ const loadHint = "pass it to load: `load(schema, { snapshot })`";
666
+ const manual = { file: schemaFile, action: "manual", importLine, loadHint };
667
+ const path = (0, import_node_path3.join)(project.root, ...schemaFile.split("/"));
668
+ if (!(0, import_node_fs2.existsSync)(path)) {
669
+ return manual;
670
+ }
671
+ const source = (0, import_node_fs2.readFileSync)(path, "utf8");
672
+ const loadCall = /load\(\s*schema\s*(\)|,\s*\{([\s\S]*?)\}\s*\))/;
673
+ const match = loadCall.exec(source);
674
+ if (match === null) {
675
+ return manual;
676
+ }
677
+ const options = match[2];
678
+ const snapshotProperty = /(?:^|[,{\s])snapshot\s*(?=[,:}]|$)/;
679
+ const snapshotInOptions = options !== void 0 && snapshotProperty.test(options);
680
+ const importPresent = source.includes(importLine);
681
+ if (snapshotInOptions && importPresent) {
682
+ return { file: schemaFile, action: "kept", importLine, loadHint };
683
+ }
684
+ let updated = source;
685
+ if (!snapshotInOptions) {
686
+ const trimmed = (options ?? "").trim();
687
+ const replacement = match[1] === ")" || trimmed.length === 0 ? "load(schema, { snapshot })" : `load(schema, { ${trimmed}, snapshot })`;
688
+ updated = updated.replace(loadCall, replacement);
689
+ }
690
+ if (!importPresent) {
691
+ updated = insertSnapshotImport(updated, importLine);
692
+ }
693
+ if (updated === source) {
694
+ return { file: schemaFile, action: "kept", importLine, loadHint };
695
+ }
696
+ (0, import_node_fs2.writeFileSync)(path, updated, "utf8");
697
+ return { file: schemaFile, action: "wired", importLine, loadHint };
698
+ }
699
+
556
700
  // src/style.ts
557
701
  function supportsColor(stream) {
558
702
  const env = process.env;
@@ -602,7 +746,7 @@ function padVisible(text, width) {
602
746
  }
603
747
 
604
748
  // src/ui.ts
605
- var import_core5 = require("@penvhq/core");
749
+ var import_core6 = require("@penvhq/core");
606
750
  var CHECK = "\u2713";
607
751
  var WARN = "\u26A0";
608
752
  var CROSS = "\u2717";
@@ -676,7 +820,7 @@ function write(lines) {
676
820
  }
677
821
  }
678
822
  function reportError(error) {
679
- if (error instanceof import_core5.PenvError) {
823
+ if (error instanceof import_core6.PenvError) {
680
824
  const suffix = error.remedy === void 0 ? void 0 : `
681
825
  ${error.remedy}`;
682
826
  const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
@@ -710,7 +854,7 @@ async function guard(run) {
710
854
  }
711
855
 
712
856
  // src/commands/push.ts
713
- var import_core6 = require("@penvhq/core");
857
+ var import_core7 = require("@penvhq/core");
714
858
  var import_citty = require("citty");
715
859
 
716
860
  // src/input.ts
@@ -774,7 +918,7 @@ async function destinationFor(project, environment, options) {
774
918
  const declared = project.config.providers[environment];
775
919
  const location = declared?.location;
776
920
  if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
777
- throw new import_core6.PenvError(
921
+ throw new import_core7.PenvError(
778
922
  "NO_DESTINATION",
779
923
  `Environment ${environment}'s provider is the local .penv tree itself, so penv has nowhere to push`,
780
924
  `Declare a provider for it in penv.config.ts \u2014 e.g. \`${environment}: { type: "@penvhq/provider-github", location: "owner/repo" }\` \u2014 or push somewhere once with \`penv push --env ${environment} --destination <package> --location <place>\`.`
@@ -801,13 +945,13 @@ function plan(resolutions, config, environment, allowDecrypt) {
801
945
  let encrypted = false;
802
946
  if (winner.file.encrypted) {
803
947
  if (!allowDecrypt) {
804
- throw new import_core6.PenvError(
948
+ throw new import_core7.PenvError(
805
949
  "ENCRYPTED_VALUE_REFUSED",
806
950
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for the destination to re-seal`,
807
951
  "Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the projection; the destination seals it under its own key."
808
952
  );
809
953
  }
810
- (0, import_core6.requireValue)(resolution, environment);
954
+ (0, import_core7.requireValue)(resolution, environment);
811
955
  encrypted = true;
812
956
  }
813
957
  if (resolution.value === void 0) {
@@ -816,7 +960,7 @@ function plan(resolutions, config, environment, allowDecrypt) {
816
960
  const scope = winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment };
817
961
  outbound.push({
818
962
  ref: resolution.ref,
819
- variable: (0, import_core6.variableName)(resolution.ref, config),
963
+ variable: (0, import_core7.variableName)(resolution.ref, config),
820
964
  value: resolution.value,
821
965
  scope,
822
966
  encrypted
@@ -854,7 +998,7 @@ async function ensureTargetApproved(provider, environment, options) {
854
998
  return false;
855
999
  }
856
1000
  if (provider.ensureTarget === void 0) {
857
- throw new import_core6.PenvError(
1001
+ throw new import_core7.PenvError(
858
1002
  "MISSING_TARGET",
859
1003
  `The destination has no environment \`${environment}\` to receive this push, and this provider cannot create one`,
860
1004
  `Create the environment on the destination side, then run \`penv push --env ${environment}\` again.`
@@ -864,7 +1008,7 @@ async function ensureTargetApproved(provider, environment, options) {
864
1008
  `The destination has no environment \`${environment}\`. Create it?`
865
1009
  );
866
1010
  if (!approved) {
867
- throw new import_core6.PenvError(
1011
+ throw new import_core7.PenvError(
868
1012
  "MISSING_TARGET",
869
1013
  `The destination has no environment \`${environment}\` to receive this push`,
870
1014
  `Re-run with \`--yes\` to create it, answer \`y\` at the prompt, or create it on the destination side yourself.`
@@ -878,7 +1022,7 @@ async function pushProjection(project, provider, environment, location, options)
878
1022
  const keys = keySourceFor(project, environment);
879
1023
  const resolutions = resolveAllSync(tree, environment, keys, true);
880
1024
  const refs = refsFrom(resolutions.map((resolution) => resolution.ref));
881
- const collision = (0, import_core6.checkNameCollisions)(refs, project.config)[0];
1025
+ const collision = (0, import_core7.checkNameCollisions)(refs, project.config)[0];
882
1026
  if (collision !== void 0) {
883
1027
  throw collision;
884
1028
  }
@@ -959,7 +1103,7 @@ async function runPush(options) {
959
1103
  const project = openProject(options.cwd);
960
1104
  const environment = targetEnvironment(project, options.environment, options.envFlags);
961
1105
  const { provider, location } = await destinationFor(project, environment, options);
962
- if ((0, import_core6.holdsProjection)(provider)) {
1106
+ if ((0, import_core7.holdsProjection)(provider)) {
963
1107
  return pushProjection(project, provider, environment, location, options);
964
1108
  }
965
1109
  return pushRecords(project, provider, environment, location);
@@ -1078,9 +1222,9 @@ var pushCommand = (0, import_citty.defineCommand)({
1078
1222
  });
1079
1223
 
1080
1224
  // src/commands/validate.ts
1081
- var import_node_path3 = require("path");
1225
+ var import_node_path4 = require("path");
1082
1226
  var import_node_url2 = require("url");
1083
- var import_core7 = require("@penvhq/core");
1227
+ var import_core8 = require("@penvhq/core");
1084
1228
  var import_citty2 = require("citty");
1085
1229
  var SCHEMA_EXPORT = "schema";
1086
1230
  var LABELS = {
@@ -1098,10 +1242,10 @@ function issueFrom(error, fallbackSubject) {
1098
1242
  message: firstLine(error.message),
1099
1243
  ...error.remedy === void 0 ? {} : { remedy: error.remedy }
1100
1244
  };
1101
- if (error instanceof import_core7.ReservedTokenError) {
1245
+ if (error instanceof import_core8.ReservedTokenError) {
1102
1246
  return { kind: "reserved", subject: error.token, ...base };
1103
1247
  }
1104
- if (error instanceof import_core7.NameCollisionError) {
1248
+ if (error instanceof import_core8.NameCollisionError) {
1105
1249
  return { kind: "collision", subject: error.variable, ...base };
1106
1250
  }
1107
1251
  return { kind: "config", subject: fallbackSubject, ...base };
@@ -1127,7 +1271,7 @@ function place(root, path, value) {
1127
1271
  function isZodType(value) {
1128
1272
  return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
1129
1273
  }
1130
- function validationIssuesOf(cause, schemaPath) {
1274
+ function validationIssuesOf(cause, schemaPath, schemaShapeFile) {
1131
1275
  if (typeof cause !== "object" || cause === null) {
1132
1276
  return void 0;
1133
1277
  }
@@ -1141,7 +1285,10 @@ function validationIssuesOf(cause, schemaPath) {
1141
1285
  kind: "schema",
1142
1286
  subject: typeof parameter === "string" && parameter !== "" ? parameter : schemaPath,
1143
1287
  message: typeof message === "string" ? message : String(issue),
1144
- remedy: `Fix the value, or adjust the schema in ${schemaPath} if the shape is wrong.`
1288
+ // `schemaPath` is the loader penv evaluated; the shape to adjust lives in
1289
+ // `schemaShapeFile`, which is that same file for an old-layout project and
1290
+ // `penv.schema.ts` for one on the split.
1291
+ remedy: `Fix the value, or adjust the schema in ${schemaShapeFile} if the shape is wrong.`
1145
1292
  };
1146
1293
  });
1147
1294
  }
@@ -1155,21 +1302,22 @@ function exclusively(work) {
1155
1302
  return result2;
1156
1303
  }
1157
1304
  function loadSchema(project, environment) {
1158
- const schemaPath = (0, import_core7.schemaFileOf)(project.config);
1159
- const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
1160
- return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
1305
+ const schemaPath = (0, import_core8.schemaFileOf)(project.config);
1306
+ const schemaShapeFile = schemaShapeFileOf(project);
1307
+ const file = (0, import_node_url2.pathToFileURL)((0, import_node_path4.resolve)(project.root, schemaPath)).href;
1308
+ return exclusively(() => loadSchemaExclusively(file, schemaPath, schemaShapeFile, environment));
1161
1309
  }
1162
- async function loadSchemaExclusively(file, schemaPath, environment) {
1163
- const jiti = (0, import_core7.jitiFor)(file);
1310
+ async function loadSchemaExclusively(file, schemaPath, schemaShapeFile, environment) {
1311
+ const jiti = (0, import_core8.jitiFor)(file);
1164
1312
  const previous = process.env.PENV_ENV;
1165
- const previousHarvest = process.env[import_core7.SCHEMA_HARVEST_ENV];
1313
+ const previousHarvest = process.env[import_core8.SCHEMA_HARVEST_ENV];
1166
1314
  process.env.PENV_ENV = environment;
1167
- process.env[import_core7.SCHEMA_HARVEST_ENV] = "1";
1315
+ process.env[import_core8.SCHEMA_HARVEST_ENV] = "1";
1168
1316
  let loaded;
1169
1317
  try {
1170
1318
  loaded = await jiti.import(file);
1171
1319
  } catch (cause) {
1172
- const issues = validationIssuesOf(cause, schemaPath);
1320
+ const issues = validationIssuesOf(cause, schemaPath, schemaShapeFile);
1173
1321
  if (issues !== void 0) {
1174
1322
  return { issues };
1175
1323
  }
@@ -1191,9 +1339,9 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
1191
1339
  process.env.PENV_ENV = previous;
1192
1340
  }
1193
1341
  if (previousHarvest === void 0) {
1194
- delete process.env[import_core7.SCHEMA_HARVEST_ENV];
1342
+ delete process.env[import_core8.SCHEMA_HARVEST_ENV];
1195
1343
  } else {
1196
- process.env[import_core7.SCHEMA_HARVEST_ENV] = previousHarvest;
1344
+ process.env[import_core8.SCHEMA_HARVEST_ENV] = previousHarvest;
1197
1345
  }
1198
1346
  }
1199
1347
  const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
@@ -1214,13 +1362,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
1214
1362
  async function runValidate(options) {
1215
1363
  const project = openProject(options.cwd);
1216
1364
  const environment = targetEnvironment(project, options.environment, options.envFlags);
1217
- const schemaPath = (0, import_core7.schemaFileOf)(project.config);
1365
+ const schemaPath = (0, import_core8.schemaFileOf)(project.config);
1366
+ const schemaShapeFile = schemaShapeFileOf(project);
1218
1367
  const issues = [];
1219
1368
  let files;
1220
1369
  try {
1221
1370
  files = await project.provider.list();
1222
1371
  } catch (error) {
1223
- if (!(error instanceof import_core7.PenvError)) {
1372
+ if (!(error instanceof import_core8.PenvError)) {
1224
1373
  throw error;
1225
1374
  }
1226
1375
  return {
@@ -1232,17 +1381,17 @@ async function runValidate(options) {
1232
1381
  };
1233
1382
  }
1234
1383
  const refs = refsFrom(files);
1235
- for (const error of (0, import_core7.validateConfig)(project.config)) {
1384
+ for (const error of (0, import_core8.validateConfig)(project.config)) {
1236
1385
  issues.push(issueFrom(error, "penv.config.ts"));
1237
1386
  }
1238
- for (const collision of (0, import_core7.checkNameCollisions)(refs, project.config)) {
1387
+ for (const collision of (0, import_core8.checkNameCollisions)(refs, project.config)) {
1239
1388
  issues.push(issueFrom(collision, collision.variable));
1240
1389
  }
1241
1390
  const { schema, issues: schemaIssues } = await loadSchema(project, environment);
1242
1391
  issues.push(...schemaIssues);
1243
1392
  let drift = EMPTY_DRIFT;
1244
1393
  if (schema !== void 0) {
1245
- const resolutions = await (0, import_core7.resolveAll)(
1394
+ const resolutions = await (0, import_core8.resolveAll)(
1246
1395
  environment,
1247
1396
  project.provider,
1248
1397
  keySourceFor(project, environment)
@@ -1251,7 +1400,7 @@ async function runValidate(options) {
1251
1400
  const undecryptable = /* @__PURE__ */ new Set();
1252
1401
  for (const resolution of resolutions) {
1253
1402
  if (resolution.undecryptable !== void 0) {
1254
- undecryptable.add((0, import_core7.accessPath)(resolution.ref).join("."));
1403
+ undecryptable.add((0, import_core8.accessPath)(resolution.ref).join("."));
1255
1404
  issues.push({
1256
1405
  kind: "undecryptable",
1257
1406
  subject: resolution.parameter,
@@ -1261,10 +1410,16 @@ async function runValidate(options) {
1261
1410
  continue;
1262
1411
  }
1263
1412
  if (resolution.value !== void 0) {
1264
- place(object, (0, import_core7.accessPath)(resolution.ref), resolution.value);
1413
+ place(object, (0, import_core8.accessPath)(resolution.ref), resolution.value);
1265
1414
  }
1266
1415
  }
1267
- drift = computeDrift({ schema, resolutions, config: project.config, environment });
1416
+ drift = computeDrift({
1417
+ schema,
1418
+ resolutions,
1419
+ config: project.config,
1420
+ environment,
1421
+ schemaShapeFile
1422
+ });
1268
1423
  const result2 = schema.safeParse(object);
1269
1424
  if (!result2.success) {
1270
1425
  for (const problem of result2.error.issues) {
@@ -1276,7 +1431,7 @@ async function runValidate(options) {
1276
1431
  kind: "schema",
1277
1432
  subject: path || schemaPath,
1278
1433
  message: problem.message,
1279
- remedy: `Fix the value, or adjust the schema in ${schemaPath} if the shape is wrong.`
1434
+ remedy: `Fix the value, or adjust the schema in ${schemaShapeFile} if the shape is wrong.`
1280
1435
  });
1281
1436
  }
1282
1437
  }
@@ -1379,7 +1534,7 @@ async function runDoctor(options) {
1379
1534
  });
1380
1535
  }
1381
1536
  }
1382
- const resolutions = await (0, import_core8.resolveAll)(
1537
+ const resolutions = await (0, import_core9.resolveAll)(
1383
1538
  environment,
1384
1539
  project.provider,
1385
1540
  keySourceFor(project, environment)
@@ -1399,13 +1554,15 @@ async function runDoctor(options) {
1399
1554
  skipped("unused", "Value coverage")
1400
1555
  );
1401
1556
  } else {
1557
+ const schemaShapeFile = schemaShapeFileOf(project);
1402
1558
  const drift = computeDrift({
1403
1559
  schema,
1404
1560
  resolutions: subjects.map(({ resolution }) => resolution),
1405
1561
  config: project.config,
1406
- environment
1562
+ environment,
1563
+ schemaShapeFile
1407
1564
  });
1408
- findings.push(...declaredFindings(drift, missing, environment));
1565
+ findings.push(...declaredFindings(drift, missing, environment, schemaShapeFile));
1409
1566
  findings.push(...weakFindings(subjects, schema));
1410
1567
  findings.push(...unusedFindings(drift));
1411
1568
  }
@@ -1421,6 +1578,8 @@ async function runDoctor(options) {
1421
1578
  findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
1422
1579
  }
1423
1580
  findings.push(...await providerDriftFindings(project, environment, options.source));
1581
+ findings.push(...snapshotStaleFindings(project));
1582
+ findings.push(...bundleInvisibleFindings(project, subjects, environment));
1424
1583
  findings.push(...await projectionFindings(project, environment, options.projection));
1425
1584
  findings.push({
1426
1585
  check: "provider",
@@ -1435,7 +1594,7 @@ async function runDoctor(options) {
1435
1594
  };
1436
1595
  }
1437
1596
  function missingFindings(subjects, environment) {
1438
- const required = subjects.filter(({ meta }) => (0, import_core8.isRequired)(meta, environment));
1597
+ const required = subjects.filter(({ meta }) => (0, import_core9.isRequired)(meta, environment));
1439
1598
  const findings = required.filter(({ resolution }) => resolution.winner === void 0).map(({ resolution }) => ({
1440
1599
  check: "missing",
1441
1600
  severity: "failure",
@@ -1456,7 +1615,7 @@ function missingFindings(subjects, environment) {
1456
1615
  }
1457
1616
  ];
1458
1617
  }
1459
- function declaredFindings(drift, missing, environment) {
1618
+ function declaredFindings(drift, missing, environment, schemaShapeFile) {
1460
1619
  const reported = new Set(missing.filter((f) => f.severity !== "pass").map((f) => f.subject));
1461
1620
  const findings = drift.declared.filter((item) => !reported.has(item.subject)).map((item) => ({
1462
1621
  check: "declared",
@@ -1474,7 +1633,7 @@ function declaredFindings(drift, missing, environment) {
1474
1633
  check: "declared",
1475
1634
  severity: "pass",
1476
1635
  label: "Schema coverage",
1477
- subject: `every parameter .penv/env.ts requires has a value for ${environment}`
1636
+ subject: `every parameter ${schemaShapeFile} requires has a value for ${environment}`
1478
1637
  }
1479
1638
  ];
1480
1639
  }
@@ -1486,7 +1645,7 @@ function weakFindings(subjects, schema) {
1486
1645
  if (value === void 0) {
1487
1646
  continue;
1488
1647
  }
1489
- const field = lookup(schema, (0, import_core8.accessPath)(resolution.ref));
1648
+ const field = lookup(schema, (0, import_core9.accessPath)(resolution.ref));
1490
1649
  if (field.kind !== "found") {
1491
1650
  continue;
1492
1651
  }
@@ -1559,7 +1718,7 @@ function fallbackFindings(subjects, environment) {
1559
1718
  ];
1560
1719
  }
1561
1720
  function plaintextSecretFindings(subjects, environment) {
1562
- const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
1721
+ const secrets = subjects.filter(({ meta }) => (0, import_core9.isSecret)(meta, environment));
1563
1722
  const findings = [];
1564
1723
  for (const { resolution } of secrets) {
1565
1724
  const winner = resolution.winner;
@@ -1599,11 +1758,11 @@ function publicSecretFindings(subjects, environment, config) {
1599
1758
  }
1600
1759
  ];
1601
1760
  }
1602
- const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
1761
+ const secrets = subjects.filter(({ meta }) => (0, import_core9.isSecret)(meta, environment));
1603
1762
  const findings = [];
1604
1763
  for (const { resolution } of secrets) {
1605
- const variable = (0, import_core8.variableName)(resolution.ref, config);
1606
- if (!(0, import_core8.isPublicVariable)(variable, config)) {
1764
+ const variable = (0, import_core9.variableName)(resolution.ref, config);
1765
+ if (!(0, import_core9.isPublicVariable)(variable, config)) {
1607
1766
  continue;
1608
1767
  }
1609
1768
  const prefix = prefixes.find((candidate) => variable.startsWith(candidate));
@@ -1689,7 +1848,7 @@ async function projectionFindings(project, environment, override) {
1689
1848
  }
1690
1849
  ];
1691
1850
  }
1692
- if (!(0, import_core8.holdsProjection)(source)) {
1851
+ if (!(0, import_core9.holdsProjection)(source)) {
1693
1852
  return [];
1694
1853
  }
1695
1854
  projection = source;
@@ -1723,7 +1882,7 @@ async function projectionFindings(project, environment, override) {
1723
1882
  }
1724
1883
  ];
1725
1884
  }
1726
- const resolutions = await (0, import_core8.resolveAll)(
1885
+ const resolutions = await (0, import_core9.resolveAll)(
1727
1886
  environment,
1728
1887
  project.provider,
1729
1888
  keySourceFor(project, environment),
@@ -1737,7 +1896,7 @@ async function projectionFindings(project, environment, override) {
1737
1896
  }
1738
1897
  expected.push({
1739
1898
  ref: resolution.ref,
1740
- variable: (0, import_core8.variableName)(resolution.ref, project.config),
1899
+ variable: (0, import_core9.variableName)(resolution.ref, project.config),
1741
1900
  scope: winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment }
1742
1901
  });
1743
1902
  }
@@ -1793,7 +1952,7 @@ async function projectionFindings(project, environment, override) {
1793
1952
  if (secret === void 0) {
1794
1953
  continue;
1795
1954
  }
1796
- const pushed = (0, import_core8.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
1955
+ const pushed = (0, import_core9.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
1797
1956
  if (typeof pushed !== "string") {
1798
1957
  continue;
1799
1958
  }
@@ -1863,7 +2022,7 @@ async function rotationSubjects(project, environment, local, override) {
1863
2022
  return { kind: "read", subjects: local };
1864
2023
  }
1865
2024
  const source = override ?? await sourceProviderFor(project, environment);
1866
- if (!(0, import_core8.holdsRecords)(source)) {
2025
+ if (!(0, import_core9.holdsRecords)(source)) {
1867
2026
  return { kind: "read", subjects: local };
1868
2027
  }
1869
2028
  try {
@@ -1890,11 +2049,11 @@ async function rotationSubjects(project, environment, local, override) {
1890
2049
  function overdueFindings(subjects, environment, now) {
1891
2050
  const findings = [];
1892
2051
  for (const { resolution, meta } of subjects) {
1893
- const { policy, lastRotated } = (0, import_core8.rotationOf)(meta, environment);
2052
+ const { policy, lastRotated } = (0, import_core9.rotationOf)(meta, environment);
1894
2053
  if (policy === void 0 || lastRotated === null) {
1895
2054
  continue;
1896
2055
  }
1897
- const interval = (0, import_core8.tryParseDuration)(policy);
2056
+ const interval = (0, import_core9.tryParseDuration)(policy);
1898
2057
  if (interval === void 0) {
1899
2058
  findings.push({
1900
2059
  check: "rotation-overdue",
@@ -1937,10 +2096,10 @@ function overdueFindings(subjects, environment, now) {
1937
2096
  function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1938
2097
  const findings = [];
1939
2098
  for (const { resolution, meta } of subjects) {
1940
- if (!(0, import_core8.isStuck)(meta, environment, now, stuckThresholdMs)) {
2099
+ if (!(0, import_core9.isStuck)(meta, environment, now, stuckThresholdMs)) {
1941
2100
  continue;
1942
2101
  }
1943
- const { rotatingSince } = (0, import_core8.rotationOf)(meta, environment);
2102
+ const { rotatingSince } = (0, import_core9.rotationOf)(meta, environment);
1944
2103
  if (rotatingSince === null) {
1945
2104
  continue;
1946
2105
  }
@@ -1980,7 +2139,7 @@ function scopeKey(scope) {
1980
2139
  case "environment-local":
1981
2140
  return `environment-local:${scope.environment}`;
1982
2141
  default:
1983
- return (0, import_core8.assertNever)(scope, "scope");
2142
+ return (0, import_core9.assertNever)(scope, "scope");
1984
2143
  }
1985
2144
  }
1986
2145
  function relevantToEnvironment(file, environment) {
@@ -2014,7 +2173,7 @@ async function providerDriftFindings(project, environment, override) {
2014
2173
  ];
2015
2174
  }
2016
2175
  const source = override ?? await sourceProviderFor(project, environment);
2017
- if (!(0, import_core8.holdsRecords)(source)) {
2176
+ if (!(0, import_core9.holdsRecords)(source)) {
2018
2177
  return [];
2019
2178
  }
2020
2179
  let local;
@@ -2040,13 +2199,13 @@ async function providerDriftFindings(project, environment, override) {
2040
2199
  const here = local.get(address);
2041
2200
  const there = remote.get(address);
2042
2201
  if (here !== void 0 && there !== void 0) {
2043
- const opened = (0, import_core8.openValue)(here.file, here.stored, keys);
2202
+ const opened = (0, import_core9.openValue)(here.file, here.stored, keys);
2044
2203
  if (opened.kind !== "plaintext") {
2045
2204
  findings.push({
2046
2205
  check: "provider-value-drift",
2047
2206
  severity: "unknown",
2048
2207
  label: "Provider value unreadable",
2049
- subject: (0, import_core8.formatValueFile)(here.file),
2208
+ subject: (0, import_core9.formatValueFile)(here.file),
2050
2209
  detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
2051
2210
  });
2052
2211
  continue;
@@ -2056,7 +2215,7 @@ async function providerDriftFindings(project, environment, override) {
2056
2215
  check: "provider-value-drift",
2057
2216
  severity: "failure",
2058
2217
  label: "Provider value drift",
2059
- subject: (0, import_core8.formatValueFile)(here.file),
2218
+ subject: (0, import_core9.formatValueFile)(here.file),
2060
2219
  detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
2061
2220
  });
2062
2221
  }
@@ -2070,7 +2229,7 @@ async function providerDriftFindings(project, environment, override) {
2070
2229
  check: "provider-value-drift",
2071
2230
  severity: "warning",
2072
2231
  label: here !== void 0 ? "Only in the local tree" : "Only in the source",
2073
- subject: (0, import_core8.formatValueFile)(present.file),
2232
+ subject: (0, import_core9.formatValueFile)(present.file),
2074
2233
  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`
2075
2234
  });
2076
2235
  }
@@ -2086,6 +2245,68 @@ async function providerDriftFindings(project, environment, override) {
2086
2245
  }
2087
2246
  ];
2088
2247
  }
2248
+ function snapshotStaleFindings(project) {
2249
+ if (!snapshotExists(project.root)) {
2250
+ return [];
2251
+ }
2252
+ const wanted = renderSnapshotModule(buildSnapshot(project));
2253
+ const committed = (0, import_node_fs3.readFileSync)((0, import_node_path5.join)(project.root, SNAPSHOT_FILE), "utf8");
2254
+ if (equalsIgnoringEol(committed, wanted)) {
2255
+ return [
2256
+ {
2257
+ check: "snapshot-stale",
2258
+ severity: "pass",
2259
+ label: "Snapshot",
2260
+ subject: `${SNAPSHOT_FILE} matches the committed sealed values and config`
2261
+ }
2262
+ ];
2263
+ }
2264
+ return [
2265
+ {
2266
+ check: "snapshot-stale",
2267
+ severity: "failure",
2268
+ label: "Snapshot stale",
2269
+ subject: SNAPSHOT_FILE,
2270
+ detail: "does not match the current sealed values or config, so a bundle resolves stale data",
2271
+ remedy: "penv snapshot"
2272
+ }
2273
+ ];
2274
+ }
2275
+ function bundleInvisibleFindings(project, subjects, environment) {
2276
+ if (!snapshotExists(project.root)) {
2277
+ return [];
2278
+ }
2279
+ const findings = [];
2280
+ for (const { resolution } of subjects) {
2281
+ const winner = resolution.winner;
2282
+ if (winner === void 0 || winner.file.encrypted) {
2283
+ continue;
2284
+ }
2285
+ const scope = winner.file.scope;
2286
+ if (scope.kind === "local" || scope.kind === "environment-local") {
2287
+ continue;
2288
+ }
2289
+ findings.push({
2290
+ check: "bundle-invisible-plaintext",
2291
+ severity: "warning",
2292
+ label: "Invisible to bundles",
2293
+ subject: winner.location,
2294
+ detail: "plaintext is gitignored and not embedded in the snapshot, so a bundle cannot see it",
2295
+ remedy: `penv encrypt ${refPathOf(resolution.ref)} --env ${environment}`
2296
+ });
2297
+ }
2298
+ if (findings.length > 0) {
2299
+ return findings;
2300
+ }
2301
+ return [
2302
+ {
2303
+ check: "bundle-invisible-plaintext",
2304
+ severity: "pass",
2305
+ label: "Bundle coverage",
2306
+ subject: `every value resolving for ${environment} is embeddable in ${SNAPSHOT_FILE}`
2307
+ }
2308
+ ];
2309
+ }
2089
2310
  var GLYPHS = {
2090
2311
  pass: CHECK,
2091
2312
  warning: WARN,
@@ -2157,11 +2378,11 @@ var doctorCommand = (0, import_citty3.defineCommand)({
2157
2378
  });
2158
2379
 
2159
2380
  // src/commands/encrypt.ts
2160
- var import_core10 = require("@penvhq/core");
2381
+ var import_core11 = require("@penvhq/core");
2161
2382
  var import_citty5 = require("citty");
2162
2383
 
2163
2384
  // src/commands/set.ts
2164
- var import_core9 = require("@penvhq/core");
2385
+ var import_core10 = require("@penvhq/core");
2165
2386
  var import_citty4 = require("citty");
2166
2387
  function scopeFrom(options) {
2167
2388
  if (options.local === true) {
@@ -2181,7 +2402,7 @@ function targetScope(project, options, key) {
2181
2402
  return scopeFrom(options);
2182
2403
  }
2183
2404
  if (environment.trim().length === 0) {
2184
- throw new import_core9.PenvError(
2405
+ throw new import_core10.PenvError(
2185
2406
  "ENVIRONMENT_FLAG_EMPTY",
2186
2407
  `\`--env\` for parameter ${key} names no environment`,
2187
2408
  `Pass a declared environment \u2014 ${project.config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
@@ -2194,27 +2415,27 @@ function policyEnvironment(project, options) {
2194
2415
  }
2195
2416
  function sealFor(project, file, value, parameter, environment) {
2196
2417
  if (environment === void 0) {
2197
- throw new import_core9.PenvError(
2418
+ throw new import_core10.PenvError(
2198
2419
  "SECRET_SCOPE_AMBIGUOUS",
2199
- `Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0, import_core9.formatValueFile)(file)} names no environment`,
2420
+ `Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0, import_core10.formatValueFile)(file)} names no environment`,
2200
2421
  "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."
2201
2422
  );
2202
2423
  }
2203
- return (0, import_core9.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
2424
+ return (0, import_core10.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
2204
2425
  }
2205
2426
  async function sealAwareWrite(options) {
2206
2427
  const { project, provider, ref, scope, value, environment } = options;
2207
- const secret = (0, import_core9.isSecret)(await provider.readMeta(ref), environment);
2428
+ const secret = (0, import_core10.isSecret)(await provider.readMeta(ref), environment);
2208
2429
  const file = {
2209
2430
  namespace: ref.namespace,
2210
2431
  name: ref.name,
2211
2432
  scope,
2212
2433
  encrypted: secret
2213
2434
  };
2214
- const stored = secret ? sealFor(project, file, value, (0, import_core9.parameterId)(ref), environment) : value;
2435
+ const stored = secret ? sealFor(project, file, value, (0, import_core10.parameterId)(ref), environment) : value;
2215
2436
  await provider.write(file, stored);
2216
2437
  await provider.remove({ ...file, encrypted: !secret });
2217
- return { encrypted: secret, location: (0, import_core9.formatValueFile)(file) };
2438
+ return { encrypted: secret, location: (0, import_core10.formatValueFile)(file) };
2218
2439
  }
2219
2440
  async function runSet(options) {
2220
2441
  const project = openProject(options.cwd);
@@ -2230,6 +2451,7 @@ async function runSet(options) {
2230
2451
  value: options.value,
2231
2452
  environment
2232
2453
  });
2454
+ refreshSnapshot(project);
2233
2455
  return { parameter: options.key, location, encrypted };
2234
2456
  }
2235
2457
  function renderSet(result2) {
@@ -2296,7 +2518,7 @@ function twins(project, key, options) {
2296
2518
  }
2297
2519
  function environmentFor(project, options, verb) {
2298
2520
  if (options.environment === void 0) {
2299
- throw new import_core10.PenvError(
2521
+ throw new import_core11.PenvError(
2300
2522
  "SECRET_SCOPE_AMBIGUOUS",
2301
2523
  `\`penv ${verb}\` names no environment, and keys are declared per environment`,
2302
2524
  "Pass `--env <environment>`. penv cannot tell which environment's key applies to a file that names none, and will not pick one for you."
@@ -2311,32 +2533,33 @@ async function runEncrypt(options) {
2311
2533
  const project = openProject(options.cwd);
2312
2534
  const environment = environmentFor(project, options, "encrypt");
2313
2535
  const [plain, sealed] = twins(project, options.key, options);
2314
- const parameter = (0, import_core10.parameterId)(plain);
2536
+ const parameter = (0, import_core11.parameterId)(plain);
2315
2537
  const value = await readOne(project, plain);
2316
2538
  if (value === void 0) {
2317
2539
  const already = await readOne(project, sealed);
2318
- throw new import_core10.PenvError(
2540
+ throw new import_core11.PenvError(
2319
2541
  "PARAMETER_ABSENT",
2320
- already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
2542
+ already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0, import_core11.formatValueFile)(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${(0, import_core11.formatValueFile)(sealed)}`,
2321
2543
  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."
2322
2544
  );
2323
2545
  }
2324
- const text = (0, import_core10.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
2546
+ const text = (0, import_core11.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
2325
2547
  await project.provider.write(sealed, text);
2326
2548
  await project.provider.remove(plain);
2549
+ refreshSnapshot(project);
2327
2550
  return {
2328
2551
  parameter,
2329
- location: (0, import_core10.formatValueFile)(sealed),
2330
- removed: (0, import_core10.formatValueFile)(plain)
2552
+ location: (0, import_core11.formatValueFile)(sealed),
2553
+ removed: (0, import_core11.formatValueFile)(plain)
2331
2554
  };
2332
2555
  }
2333
2556
  async function runDecrypt(options) {
2334
2557
  const project = openProject(options.cwd);
2335
2558
  const environment = environmentFor(project, options, "decrypt");
2336
2559
  const [plain, sealed] = twins(project, options.key, options);
2337
- const parameter = (0, import_core10.parameterId)(plain);
2338
- if ((0, import_core10.isSecret)(await project.provider.readMeta(plain), environment)) {
2339
- throw new import_core10.PenvError(
2560
+ const parameter = (0, import_core11.parameterId)(plain);
2561
+ if ((0, import_core11.isSecret)(await project.provider.readMeta(plain), environment)) {
2562
+ throw new import_core11.PenvError(
2340
2563
  "SECRET_DECRYPT_REFUSED",
2341
2564
  `Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
2342
2565
  "A secret with a plaintext value file is a `penv doctor` failure. Drop `secret` from the parameter's meta if it is not one, or run `penv generate --allow-decrypt` if you need the plaintext value in a `.env` artifact."
@@ -2344,30 +2567,31 @@ async function runDecrypt(options) {
2344
2567
  }
2345
2568
  const stored = await readOne(project, sealed);
2346
2569
  if (stored === void 0) {
2347
- throw new import_core10.PenvError(
2570
+ throw new import_core11.PenvError(
2348
2571
  "PARAMETER_ABSENT",
2349
- `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
2572
+ `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0, import_core11.formatValueFile)(sealed)}`,
2350
2573
  `Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
2351
2574
  );
2352
2575
  }
2353
- const opened = (0, import_core10.openValue)(sealed, stored, keySourceFor(project, environment));
2576
+ const opened = (0, import_core11.openValue)(sealed, stored, keySourceFor(project, environment));
2354
2577
  if (opened.kind === "failed") {
2355
2578
  throw new UndecryptableAt(
2356
2579
  parameter,
2357
2580
  environment,
2358
- (0, import_core10.formatValueFile)(sealed),
2581
+ (0, import_core11.formatValueFile)(sealed),
2359
2582
  opened.failure.detail
2360
2583
  );
2361
2584
  }
2362
2585
  await project.provider.write(plain, opened.value);
2363
2586
  await project.provider.remove(sealed);
2587
+ refreshSnapshot(project);
2364
2588
  return {
2365
2589
  parameter,
2366
- location: (0, import_core10.formatValueFile)(plain),
2367
- removed: (0, import_core10.formatValueFile)(sealed)
2590
+ location: (0, import_core11.formatValueFile)(plain),
2591
+ removed: (0, import_core11.formatValueFile)(sealed)
2368
2592
  };
2369
2593
  }
2370
- var UndecryptableAt = class extends import_core10.PenvError {
2594
+ var UndecryptableAt = class extends import_core11.PenvError {
2371
2595
  constructor(parameter, environment, location, detail) {
2372
2596
  super(
2373
2597
  "VALUE_UNDECRYPTABLE",
@@ -2422,7 +2646,7 @@ var decryptCommand = (0, import_citty5.defineCommand)({
2422
2646
  });
2423
2647
 
2424
2648
  // src/commands/fill.ts
2425
- var import_core11 = require("@penvhq/core");
2649
+ var import_core12 = require("@penvhq/core");
2426
2650
  var import_citty6 = require("citty");
2427
2651
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
2428
2652
  async function runFill(options) {
@@ -2437,7 +2661,7 @@ async function runFill(options) {
2437
2661
  const detail = blockers.map(
2438
2662
  (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
2439
2663
  ).join("\n");
2440
- throw new import_core11.PenvError(
2664
+ throw new import_core12.PenvError(
2441
2665
  "FILL_BLOCKED",
2442
2666
  `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
2443
2667
  ${detail}`,
@@ -2572,16 +2796,16 @@ var fillCommand = (0, import_citty6.defineCommand)({
2572
2796
  });
2573
2797
 
2574
2798
  // src/commands/generate.ts
2575
- var import_node_fs = require("fs");
2576
- var import_node_path4 = require("path");
2577
- var import_core12 = require("@penvhq/core");
2799
+ var import_node_fs4 = require("fs");
2800
+ var import_node_path6 = require("path");
2801
+ var import_core13 = require("@penvhq/core");
2578
2802
  var import_citty7 = require("citty");
2579
2803
  var DEFAULT_OUTPUT = ".env";
2580
2804
  function entriesFor(project, environment, allowDecrypt) {
2581
2805
  const keys = keySourceFor(project, environment);
2582
2806
  const tree = localTree(project);
2583
2807
  const resolutions = resolveAllSync(tree, environment, keys);
2584
- const collision = (0, import_core12.checkNameCollisions)(
2808
+ const collision = (0, import_core13.checkNameCollisions)(
2585
2809
  refsFrom(resolutions.map((resolution) => resolution.ref)),
2586
2810
  project.config
2587
2811
  )[0];
@@ -2594,21 +2818,21 @@ function entriesFor(project, environment, allowDecrypt) {
2594
2818
  const winner = resolution.winner;
2595
2819
  if (winner?.file.encrypted === true) {
2596
2820
  if (!allowDecrypt) {
2597
- throw new import_core12.PenvError(
2821
+ throw new import_core13.PenvError(
2598
2822
  "ENCRYPTED_VALUE_REFUSED",
2599
2823
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
2600
2824
  `Re-run with \`--allow-decrypt\` to write the decrypted value into the artifact, or generate for an environment whose values are plaintext. The artifact is gitignored; a committed plaintext secret is a \`penv doctor\` failure.`
2601
2825
  );
2602
2826
  }
2603
- (0, import_core12.requireValue)(resolution, environment);
2827
+ (0, import_core13.requireValue)(resolution, environment);
2604
2828
  decrypted += 1;
2605
2829
  }
2606
2830
  if (resolution.value === void 0) {
2607
2831
  continue;
2608
2832
  }
2609
- const description = (0, import_core12.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
2833
+ const description = (0, import_core13.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
2610
2834
  entries.push({
2611
- key: (0, import_core12.variableName)(resolution.ref, project.config),
2835
+ key: (0, import_core13.variableName)(resolution.ref, project.config),
2612
2836
  value: resolution.value,
2613
2837
  ...typeof description === "string" ? { description } : {}
2614
2838
  });
@@ -2619,18 +2843,18 @@ function entriesFor(project, environment, allowDecrypt) {
2619
2843
  function generateDotenv(options) {
2620
2844
  const project = openProject(options.cwd);
2621
2845
  const environment = targetEnvironment(project, options.environment, options.envFlags);
2622
- return (0, import_core12.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
2846
+ return (0, import_core13.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
2623
2847
  }
2624
2848
  function runGenerate(options) {
2625
2849
  const project = openProject(options.cwd);
2626
2850
  const environment = targetEnvironment(project, options.environment, options.envFlags);
2627
2851
  const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
2628
- const file = options.out === void 0 ? (0, import_node_path4.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path4.isAbsolute)(options.out) ? options.out : (0, import_node_path4.resolve)(options.cwd, options.out);
2629
- (0, import_node_fs.writeFileSync)(file, (0, import_core12.serializeDotenv)(entries), "utf8");
2852
+ const file = options.out === void 0 ? (0, import_node_path6.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path6.isAbsolute)(options.out) ? options.out : (0, import_node_path6.resolve)(options.cwd, options.out);
2853
+ (0, import_node_fs4.writeFileSync)(file, (0, import_core13.serializeDotenv)(entries), "utf8");
2630
2854
  return { file, environment, entries: entries.length, decrypted };
2631
2855
  }
2632
2856
  function displayPath(cwd, file) {
2633
- const rel = (0, import_node_path4.relative)(cwd, file);
2857
+ const rel = (0, import_node_path6.relative)(cwd, file);
2634
2858
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
2635
2859
  }
2636
2860
  function renderGenerate(result2, cwd) {
@@ -2678,17 +2902,17 @@ var generateCommand = (0, import_citty7.defineCommand)({
2678
2902
  });
2679
2903
 
2680
2904
  // src/commands/get.ts
2681
- var import_core13 = require("@penvhq/core");
2905
+ var import_core14 = require("@penvhq/core");
2682
2906
  var import_citty8 = require("citty");
2683
2907
  async function runGet(options) {
2684
2908
  const project = openProject(options.cwd);
2685
2909
  const environment = targetEnvironment(project, options.environment);
2686
2910
  const ref = refFromKey(options.key);
2687
2911
  const keys = keySourceFor(project, environment);
2688
- const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
2689
- const value = (0, import_core13.requireValue)(resolution, environment);
2912
+ const resolution = await (0, import_core14.resolveParameter)(ref, environment, project.provider, keys);
2913
+ const value = (0, import_core14.requireValue)(resolution, environment);
2690
2914
  if (value === void 0) {
2691
- throw new import_core13.PenvError(
2915
+ throw new import_core14.PenvError(
2692
2916
  "PARAMETER_ABSENT",
2693
2917
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
2694
2918
  `Set it with \`penv set ${options.key} --env ${environment}\`, or run \`penv get ${options.key} --env ${environment} --explain\` to see every file penv looked at.`
@@ -2710,7 +2934,7 @@ async function runExplain(options) {
2710
2934
  const environment = targetEnvironment(project, options.environment);
2711
2935
  const ref = refFromKey(options.key);
2712
2936
  const keys = keySourceFor(project, environment);
2713
- const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
2937
+ const resolution = await (0, import_core14.resolveParameter)(ref, environment, project.provider, keys);
2714
2938
  const winner = resolution.winner;
2715
2939
  return {
2716
2940
  parameter: resolution.parameter,
@@ -2764,15 +2988,15 @@ var getCommand = (0, import_citty8.defineCommand)({
2764
2988
  });
2765
2989
 
2766
2990
  // src/commands/import.ts
2767
- var import_node_fs4 = require("fs");
2768
- var import_node_path7 = require("path");
2769
- var import_core16 = require("@penvhq/core");
2991
+ var import_node_fs7 = require("fs");
2992
+ var import_node_path9 = require("path");
2993
+ var import_core17 = require("@penvhq/core");
2770
2994
  var import_citty10 = require("citty");
2771
2995
 
2772
2996
  // src/detect.ts
2773
- var import_node_fs2 = require("fs");
2774
- var import_node_path5 = require("path");
2775
- var import_core14 = require("@penvhq/core");
2997
+ var import_node_fs5 = require("fs");
2998
+ var import_node_path7 = require("path");
2999
+ var import_core15 = require("@penvhq/core");
2776
3000
  var SIGNATURES = [
2777
3001
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
2778
3002
  { name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
@@ -2788,7 +3012,7 @@ var SIGNATURES = [
2788
3012
  function exportsSchema(file) {
2789
3013
  let source;
2790
3014
  try {
2791
- source = (0, import_node_fs2.readFileSync)(file, "utf8");
3015
+ source = (0, import_node_fs5.readFileSync)(file, "utf8");
2792
3016
  } catch {
2793
3017
  return false;
2794
3018
  }
@@ -2799,8 +3023,8 @@ function exportsSchema(file) {
2799
3023
  );
2800
3024
  }
2801
3025
  function occupied(cwd, relative3) {
2802
- const file = (0, import_node_path5.join)(cwd, ...relative3.split("/"));
2803
- return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
3026
+ const file = (0, import_node_path7.join)(cwd, ...relative3.split("/"));
3027
+ return (0, import_node_fs5.existsSync)(file) && !exportsSchema(file);
2804
3028
  }
2805
3029
  function schemaFileFor(cwd) {
2806
3030
  const dir = srcPrefix(cwd);
@@ -2812,10 +3036,10 @@ function schemaFileFor(cwd) {
2812
3036
  if (!occupied(cwd, beside)) {
2813
3037
  return { file: beside, displaced: preferred };
2814
3038
  }
2815
- return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
3039
+ return { file: import_core15.DEFAULT_SCHEMA_FILE, displaced: preferred };
2816
3040
  }
2817
3041
  function srcPrefix(cwd) {
2818
- return (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
3042
+ return (0, import_node_fs5.existsSync)((0, import_node_path7.join)(cwd, "src")) ? "src/" : "";
2819
3043
  }
2820
3044
  function dependenciesOf(cwd) {
2821
3045
  const manifest = manifestOf(cwd);
@@ -2834,13 +3058,13 @@ function dependenciesOf(cwd) {
2834
3058
  return names;
2835
3059
  }
2836
3060
  function manifestOf(cwd) {
2837
- const file = (0, import_node_path5.join)(cwd, "package.json");
2838
- if (!(0, import_node_fs2.existsSync)(file)) {
3061
+ const file = (0, import_node_path7.join)(cwd, "package.json");
3062
+ if (!(0, import_node_fs5.existsSync)(file)) {
2839
3063
  return void 0;
2840
3064
  }
2841
3065
  let manifest;
2842
3066
  try {
2843
- manifest = JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
3067
+ manifest = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
2844
3068
  } catch {
2845
3069
  return void 0;
2846
3070
  }
@@ -2856,7 +3080,7 @@ function detectFramework(cwd) {
2856
3080
  return detectedFrom(cwd, signature.name, signature.publicPrefixes);
2857
3081
  }
2858
3082
  }
2859
- if ((0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
3083
+ if ((0, import_node_fs5.existsSync)((0, import_node_path7.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
2860
3084
  return detectedFrom(cwd, "Bun", []);
2861
3085
  }
2862
3086
  return void 0;
@@ -2882,10 +3106,10 @@ function hasImportsBlock(cwd) {
2882
3106
  }
2883
3107
 
2884
3108
  // src/commands/init.ts
2885
- var import_node_fs3 = require("fs");
2886
- var import_node_path6 = require("path");
3109
+ var import_node_fs6 = require("fs");
3110
+ var import_node_path8 = require("path");
2887
3111
  var import_promises = require("readline/promises");
2888
- var import_core15 = require("@penvhq/core");
3112
+ var import_core16 = require("@penvhq/core");
2889
3113
  var import_citty9 = require("citty");
2890
3114
 
2891
3115
  // src/seams.ts
@@ -3026,6 +3250,7 @@ function seamFor(framework, context) {
3026
3250
  }
3027
3251
 
3028
3252
  // src/commands/init.ts
3253
+ var SCHEMA_SHAPE_BASENAME = "penv.schema";
3029
3254
  var CONFIG_FILE = "penv.config.ts";
3030
3255
  var TSCONFIG_FILE = "tsconfig.json";
3031
3256
  var GITIGNORE_FILE = ".gitignore";
@@ -3036,15 +3261,15 @@ var PACKAGE_FILE = "package.json";
3036
3261
  var DEFAULT_DECISIONS = {
3037
3262
  environments: [],
3038
3263
  inject: false,
3039
- schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
3264
+ schemaFile: import_core16.DEFAULT_SCHEMA_FILE,
3040
3265
  publicPrefixes: [],
3041
3266
  alias: DEFAULT_ALIAS
3042
3267
  };
3043
- var NOT_ENVIRONMENTS = [...import_core15.RESERVED_TOKENS, "example", "sample", "template"];
3268
+ var NOT_ENVIRONMENTS = [...import_core16.RESERVED_TOKENS, "example", "sample", "template"];
3044
3269
  function suggestEnvironments(root) {
3045
3270
  let entries;
3046
3271
  try {
3047
- entries = (0, import_node_fs3.readdirSync)(root);
3272
+ entries = (0, import_node_fs6.readdirSync)(root);
3048
3273
  } catch {
3049
3274
  return [];
3050
3275
  }
@@ -3056,7 +3281,7 @@ function suggestEnvironments(root) {
3056
3281
  const segments = entry.slice(".env.".length).split(".");
3057
3282
  const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
3058
3283
  const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
3059
- if (name === void 0 || !(0, import_core15.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
3284
+ if (name === void 0 || !(0, import_core16.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
3060
3285
  continue;
3061
3286
  }
3062
3287
  found.add(name);
@@ -3064,10 +3289,10 @@ function suggestEnvironments(root) {
3064
3289
  return [...found].sort();
3065
3290
  }
3066
3291
  function emptyFlag(flag) {
3067
- return new import_core15.PenvError(
3292
+ return new import_core16.PenvError(
3068
3293
  "INIT_FLAG_EMPTY",
3069
3294
  `\`--${flag}\` was given without a value`,
3070
- flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core15.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
3295
+ flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core16.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
3071
3296
  );
3072
3297
  }
3073
3298
  function splitEnvironments(value) {
@@ -3088,11 +3313,11 @@ function configOf(decisions) {
3088
3313
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
3089
3314
  }
3090
3315
  function declaredIn(root) {
3091
- const file = (0, import_node_path6.join)(root, CONFIG_FILE);
3092
- if (!(0, import_node_fs3.existsSync)(file)) {
3316
+ const file = (0, import_node_path8.join)(root, CONFIG_FILE);
3317
+ if (!(0, import_node_fs6.existsSync)(file)) {
3093
3318
  return void 0;
3094
3319
  }
3095
- return (0, import_core15.loadConfigFrom)(file);
3320
+ return (0, import_core16.loadConfigFrom)(file);
3096
3321
  }
3097
3322
  function planInit(root, flags = {}) {
3098
3323
  const declared = declaredIn(root);
@@ -3102,7 +3327,7 @@ function planInit(root, flags = {}) {
3102
3327
  notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
3103
3328
  } else if (detected === void 0) {
3104
3329
  notes.push(
3105
- `No framework detected in package.json \u2014 the schema goes to ${import_core15.DEFAULT_SCHEMA_FILE}.`
3330
+ `No framework detected in package.json \u2014 the schema goes to ${import_core16.DEFAULT_SCHEMA_FILE}.`
3106
3331
  );
3107
3332
  } else {
3108
3333
  notes.push(`Detected ${detected.name}.`);
@@ -3112,12 +3337,12 @@ function planInit(root, flags = {}) {
3112
3337
  );
3113
3338
  }
3114
3339
  }
3115
- const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core15.schemaFileOf)(declared) : detected?.schemaFile ?? import_core15.DEFAULT_SCHEMA_FILE : flags.schema.trim();
3340
+ const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core16.schemaFileOf)(declared) : detected?.schemaFile ?? import_core16.DEFAULT_SCHEMA_FILE : flags.schema.trim();
3116
3341
  if (flags.schema !== void 0) {
3117
3342
  if (schemaFile.length === 0) {
3118
3343
  throw emptyFlag("schema");
3119
3344
  }
3120
- const error = (0, import_core15.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
3345
+ const error = (0, import_core16.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
3121
3346
  if (error !== void 0) {
3122
3347
  throw error;
3123
3348
  }
@@ -3127,7 +3352,7 @@ function planInit(root, flags = {}) {
3127
3352
  throw emptyFlag("alias");
3128
3353
  }
3129
3354
  if (!ALIAS_NAME.test(alias)) {
3130
- throw new import_core15.PenvError(
3355
+ throw new import_core16.PenvError(
3131
3356
  "INIT_ALIAS_INVALID",
3132
3357
  `\`${alias}\` is not an alias penv can write`,
3133
3358
  `An alias is \`@name\` \u2014 a tsconfig \`paths\` entry a bundler resolves \u2014 or \`#name\`, a package.json \`imports\` entry Node resolves itself. Those are the two things a module specifier can be that is not a package.`
@@ -3177,7 +3402,7 @@ function renderPlan(plan2) {
3177
3402
  rows.push([
3178
3403
  ` ${out.dim("schemaFile")}`,
3179
3404
  plan2.decisions.schemaFile,
3180
- plan2.decisions.schemaFile === import_core15.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core15.DEFAULT_SCHEMA_FILE})`)
3405
+ plan2.decisions.schemaFile === import_core16.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core16.DEFAULT_SCHEMA_FILE})`)
3181
3406
  ]);
3182
3407
  for (const prefix of plan2.decisions.publicPrefixes) {
3183
3408
  rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
@@ -3227,20 +3452,17 @@ async function askInject(io) {
3227
3452
  }
3228
3453
  var EMPTY_SCHEMA_BODY = " // One key per parameter, e.g. `databaseUrl: z.url(),`. Nesting a key nests\n // the parameter: `redis: z.object({ password: z.string() })` is redis/password.";
3229
3454
  var DRAFT_HEADER = "// DRAFT \u2014 generated by `penv import` from one sample of each value, and yours\n// to correct. Single-sample inference cannot know that a boolean seen as `true`\n// must also accept `1`/`0`, or that a string is really a URL. penv scaffolds\n// this file once and never regenerates it, so edits here are safe.\n";
3230
- function renderSchemaModule(fields, draft, inject = false) {
3455
+ function renderSchemaShapeModule(fields, draft) {
3231
3456
  const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
3232
- const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly (WorkOS, Prisma) instead of importing @env.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid.\n";
3233
- const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
3234
3457
  return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
3235
- import { load } from "@penvhq/penv";
3236
3458
 
3237
- // The shape. Import this (or z.infer<typeof schema>) when you only need the
3238
- // type \u2014 tests, tooling \u2014 so you don't trigger config loading.
3459
+ // The shape \u2014 the one definition every consumer derives from. Import this (or
3460
+ // z.infer<typeof schema>) when you only need the type \u2014 tests, tooling \u2014 so you
3461
+ // don't trigger config loading. .penv/env.ts wraps it with load().
3239
3462
  export const schema = z.object({
3240
3463
  ${body}
3241
3464
  });
3242
3465
 
3243
- ` + loadComment + loadCall + `
3244
3466
  // Registers the schema's shape with penv's types (erased at runtime, so
3245
3467
  // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3246
3468
  // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
@@ -3251,6 +3473,24 @@ declare module "@penvhq/core" {
3251
3473
  }
3252
3474
  `;
3253
3475
  }
3476
+ function shapeSpecifier(schemaFile) {
3477
+ const depth = schemaFile.split("/").length - 1;
3478
+ const climb = depth === 0 ? "./" : "../".repeat(depth);
3479
+ return `${climb}${SCHEMA_SHAPE_BASENAME}.js`;
3480
+ }
3481
+ function renderEnvModule(schemaFile, inject = false) {
3482
+ const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly. `snapshot` lets load() resolve in a bundled or\n// serverless runtime where no penv.config.ts is on disk.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid. `snapshot` lets load()\n// resolve in a bundled or serverless runtime where no penv.config.ts is on disk.\n";
3483
+ const loadCall = inject ? "export const env = load(schema, { inject: true, snapshot });\n" : "export const env = load(schema, { snapshot });\n";
3484
+ return `import { load } from "@penvhq/penv";
3485
+ import { schema } from "${shapeSpecifier(schemaFile)}";
3486
+ import { snapshot } from "${snapshotSpecifier(schemaFile)}";
3487
+
3488
+ // Re-exported so type-only consumers can import the shape through @env without
3489
+ // triggering config loading \u2014 the shape itself lives in ${import_core16.SCHEMA_SHAPE_FILE}.
3490
+ export { schema };
3491
+
3492
+ ` + loadComment + loadCall;
3493
+ }
3254
3494
  function renderEnvironments(decisions) {
3255
3495
  const shared = " // Environments are a whitelist. A filename segment is an environment only if\n // it is declared here \u2014 penv never infers one from a folder or a filename.\n";
3256
3496
  if (decisions.environments.length === 0) {
@@ -3258,7 +3498,7 @@ function renderEnvironments(decisions) {
3258
3498
  // can read off your codebase, and an environment you do not have is worse
3259
3499
  // than one you have not declared yet. Name yours, and give each a provider:
3260
3500
  // environments: ["development", "production"],
3261
- // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
3501
+ // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
3262
3502
  environments: [],
3263
3503
 
3264
3504
  providers: {},
@@ -3276,7 +3516,7 @@ ${providers} },
3276
3516
  }
3277
3517
  function renderConfigModule(decisions) {
3278
3518
  let body = renderEnvironments(decisions);
3279
- if (decisions.schemaFile !== import_core15.DEFAULT_SCHEMA_FILE) {
3519
+ if (decisions.schemaFile !== import_core16.DEFAULT_SCHEMA_FILE) {
3280
3520
  body += `
3281
3521
  // The module that exports the schema. It is yours \u2014 penv scaffolds it once
3282
3522
  // and never regenerates it \u2014 so this says where you keep it.
@@ -3299,7 +3539,7 @@ ${body}});
3299
3539
  `;
3300
3540
  }
3301
3541
  function renderGitignore(decisions) {
3302
- const inside = (0, import_core15.schemaInsideTree)(configOf(decisions));
3542
+ const inside = (0, import_core16.schemaInsideTree)(configOf(decisions));
3303
3543
  const listed = inside === void 0 ? "" : `${inside}, `;
3304
3544
  return `# Written by penv. Value files hold configuration values and are never
3305
3545
  # committed; only the structure, ${listed}meta, and config are.
@@ -3444,13 +3684,13 @@ ${objectIndent}${source.slice(close)}`;
3444
3684
  ${entryIndent}${member},${source.slice(open + 1)}`;
3445
3685
  }
3446
3686
  function shapeError(what, target, alias) {
3447
- return new import_core15.PenvError(
3687
+ return new import_core16.PenvError(
3448
3688
  "TSCONFIG_SHAPE",
3449
3689
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
3450
3690
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
3451
3691
  );
3452
3692
  }
3453
- function insertEnvAlias(source, target = import_core15.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
3693
+ function insertEnvAlias(source, target = import_core16.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
3454
3694
  const alias = `"${name}": ["${target}"]`;
3455
3695
  const root = skipTrivia(source, 0);
3456
3696
  if (source.charAt(root) !== "{") {
@@ -3514,46 +3754,102 @@ function renderTsconfig(target, alias) {
3514
3754
  `;
3515
3755
  }
3516
3756
  function ensurePenvDir(root) {
3517
- const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
3518
- if ((0, import_node_fs3.existsSync)(dir)) {
3757
+ const dir = (0, import_node_path8.resolve)(root, PENV_DIR2);
3758
+ if ((0, import_node_fs6.existsSync)(dir)) {
3519
3759
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
3520
3760
  }
3521
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
3761
+ (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
3522
3762
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
3523
3763
  }
3524
- function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3525
- const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
3526
- if ((0, import_node_fs3.existsSync)(file)) {
3764
+ function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3765
+ const file = (0, import_node_path8.join)(root, import_core16.SCHEMA_SHAPE_FILE);
3766
+ if ((0, import_node_fs6.existsSync)(file)) {
3527
3767
  return {
3528
3768
  target: "schema",
3529
3769
  action: "kept",
3530
- text: `Kept ${decisions.schemaFile}`,
3770
+ text: `Kept ${import_core16.SCHEMA_SHAPE_FILE}`,
3531
3771
  note: "(yours \u2014 penv never regenerates it)"
3532
3772
  };
3533
3773
  }
3534
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3535
- (0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
3774
+ const oldLayout = selfContainedSchemaModule(root, decisions.schemaFile);
3775
+ if (oldLayout !== void 0) {
3776
+ return {
3777
+ target: "schema",
3778
+ action: "conflicted",
3779
+ text: `Kept your schema in ${oldLayout}`,
3780
+ note: `(the shape lives there, from before the ${import_core16.SCHEMA_SHAPE_FILE} split \u2014 penv did not add a second one. To adopt the split: move the \`z.object\` (and any \`declare module\` block) into ${import_core16.SCHEMA_SHAPE_FILE}, leaving ${oldLayout} importing \`schema\` from it and calling \`load\`.)`
3781
+ };
3782
+ }
3783
+ (0, import_node_fs6.writeFileSync)(file, renderSchemaShapeModule(fields, draft), "utf8");
3536
3784
  return {
3537
3785
  target: "schema",
3538
3786
  action: "created",
3787
+ text: `Generated ${import_core16.SCHEMA_SHAPE_FILE}`,
3788
+ note: draft ? "(draft schema \u2014 review it, it's yours)" : "(the shape \u2014 yours to edit)"
3789
+ };
3790
+ }
3791
+ function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
3792
+ const file = (0, import_node_path8.join)(root, ...decisions.schemaFile.split("/"));
3793
+ if ((0, import_node_fs6.existsSync)(file)) {
3794
+ return {
3795
+ target: "env",
3796
+ action: "kept",
3797
+ text: `Kept ${decisions.schemaFile}`,
3798
+ note: "(yours \u2014 penv never regenerates it)"
3799
+ };
3800
+ }
3801
+ (0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
3802
+ (0, import_node_fs6.writeFileSync)(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
3803
+ return {
3804
+ target: "env",
3805
+ action: "created",
3539
3806
  text: `Generated ${decisions.schemaFile}`,
3540
- note: draft ? "(draft schema \u2014 review it, it's yours)" : "(schema + loader \u2014 yours to edit)"
3807
+ note: "(loads the shape \u2014 yours to edit)"
3541
3808
  };
3542
3809
  }
3543
3810
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
3544
- const file = (0, import_node_path6.join)(root, CONFIG_FILE);
3545
- if ((0, import_node_fs3.existsSync)(file)) {
3811
+ const file = (0, import_node_path8.join)(root, CONFIG_FILE);
3812
+ if ((0, import_node_fs6.existsSync)(file)) {
3546
3813
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
3547
3814
  }
3548
- (0, import_node_fs3.writeFileSync)(file, renderConfigModule(decisions), "utf8");
3815
+ (0, import_node_fs6.writeFileSync)(file, renderConfigModule(decisions), "utf8");
3549
3816
  return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
3550
3817
  }
3818
+ function snapshotConfig(decisions) {
3819
+ const providers = {};
3820
+ for (const environment of decisions.environments) {
3821
+ providers[environment] = { type: "@penvhq/provider-filesystem" };
3822
+ }
3823
+ return {
3824
+ environments: decisions.environments,
3825
+ providers,
3826
+ ...decisions.schemaFile === import_core16.DEFAULT_SCHEMA_FILE ? {} : { schemaFile: decisions.schemaFile },
3827
+ ...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: decisions.publicPrefixes }
3828
+ };
3829
+ }
3830
+ function writeSnapshotModule(root, decisions = DEFAULT_DECISIONS) {
3831
+ const file = (0, import_node_path8.join)(root, SNAPSHOT_FILE);
3832
+ if ((0, import_node_fs6.existsSync)(file)) {
3833
+ return { target: "snapshot", action: "kept", text: `Kept ${SNAPSHOT_FILE}` };
3834
+ }
3835
+ (0, import_node_fs6.writeFileSync)(
3836
+ file,
3837
+ renderSnapshotModule(buildSnapshotFrom(root, snapshotConfig(decisions))),
3838
+ "utf8"
3839
+ );
3840
+ return {
3841
+ target: "snapshot",
3842
+ action: "created",
3843
+ text: `Generated ${SNAPSHOT_FILE}`,
3844
+ note: "(committed \u2014 the bundle resolves load() from it)"
3845
+ };
3846
+ }
3551
3847
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3552
3848
  const alias = decisions.alias;
3553
3849
  const imports = alias.startsWith(IMPORTS_PREFIX);
3554
- const file = (0, import_node_path6.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
3850
+ const file = (0, import_node_path8.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
3555
3851
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
3556
- if (!(0, import_node_fs3.existsSync)(file)) {
3852
+ if (!(0, import_node_fs6.existsSync)(file)) {
3557
3853
  if (imports) {
3558
3854
  return {
3559
3855
  target: "tsconfig",
@@ -3562,14 +3858,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3562
3858
  note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
3563
3859
  };
3564
3860
  }
3565
- (0, import_node_fs3.writeFileSync)(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
3861
+ (0, import_node_fs6.writeFileSync)(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
3566
3862
  return {
3567
3863
  target: "tsconfig",
3568
3864
  action: "created",
3569
3865
  text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
3570
3866
  };
3571
3867
  }
3572
- const source = (0, import_node_fs3.readFileSync)(file, "utf8");
3868
+ const source = (0, import_node_fs6.readFileSync)(file, "utf8");
3573
3869
  const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
3574
3870
  if (edit.conflict !== void 0) {
3575
3871
  return {
@@ -3586,7 +3882,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3586
3882
  text: `Kept the ${alias} alias in ${where}`
3587
3883
  };
3588
3884
  }
3589
- (0, import_node_fs3.writeFileSync)(file, edit.source, "utf8");
3885
+ (0, import_node_fs6.writeFileSync)(file, edit.source, "utf8");
3590
3886
  return {
3591
3887
  target: "tsconfig",
3592
3888
  action: "updated",
@@ -3594,15 +3890,15 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3594
3890
  };
3595
3891
  }
3596
3892
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
3597
- const file = (0, import_node_path6.join)(root, PENV_DIR2, GITIGNORE_FILE);
3893
+ const file = (0, import_node_path8.join)(root, PENV_DIR2, GITIGNORE_FILE);
3598
3894
  const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
3599
3895
  const wanted = renderGitignore(decisions);
3600
- const existing = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : void 0;
3896
+ const existing = (0, import_node_fs6.existsSync)(file) ? (0, import_node_fs6.readFileSync)(file, "utf8") : void 0;
3601
3897
  if (existing === wanted) {
3602
3898
  return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
3603
3899
  }
3604
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
3605
- (0, import_node_fs3.writeFileSync)(file, wanted, "utf8");
3900
+ (0, import_node_fs6.mkdirSync)((0, import_node_path8.join)(root, PENV_DIR2), { recursive: true });
3901
+ (0, import_node_fs6.writeFileSync)(file, wanted, "utf8");
3606
3902
  return {
3607
3903
  target: "gitignore",
3608
3904
  action: existing === void 0 ? "created" : "updated",
@@ -3618,12 +3914,12 @@ function outdatedRuntimeWarning(root) {
3618
3914
  return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
3619
3915
  }
3620
3916
  function installedPenvVersion(root) {
3621
- const file = (0, import_node_path6.join)(root, "node_modules", "@penvhq", "penv", "package.json");
3622
- if (!(0, import_node_fs3.existsSync)(file)) {
3917
+ const file = (0, import_node_path8.join)(root, "node_modules", "@penvhq", "penv", "package.json");
3918
+ if (!(0, import_node_fs6.existsSync)(file)) {
3623
3919
  return void 0;
3624
3920
  }
3625
3921
  try {
3626
- const version = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8")).version;
3922
+ const version = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8")).version;
3627
3923
  return typeof version === "string" ? version : void 0;
3628
3924
  } catch {
3629
3925
  return void 0;
@@ -3663,11 +3959,11 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramew
3663
3959
  };
3664
3960
  }
3665
3961
  const alsoNote = writeAlso(root, seam.also);
3666
- const file = (0, import_node_path6.join)(root, ...seam.file.split("/"));
3962
+ const file = (0, import_node_path8.join)(root, ...seam.file.split("/"));
3667
3963
  const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
3668
3964
  const notes = baseNotes.length === 0 ? "" : `
3669
3965
  ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
3670
- if ((0, import_node_fs3.existsSync)(file)) {
3966
+ if ((0, import_node_fs6.existsSync)(file)) {
3671
3967
  return {
3672
3968
  target: "seam",
3673
3969
  action: "info",
@@ -3675,8 +3971,8 @@ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
3675
3971
  note: withWarning(`${seam.ifPresent}${notes}`, outdated)
3676
3972
  };
3677
3973
  }
3678
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3679
- (0, import_node_fs3.writeFileSync)(file, seam.content, "utf8");
3974
+ (0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
3975
+ (0, import_node_fs6.writeFileSync)(file, seam.content, "utf8");
3680
3976
  return {
3681
3977
  target: "seam",
3682
3978
  action: outdated === void 0 ? "created" : "info",
@@ -3688,12 +3984,12 @@ function writeAlso(root, also) {
3688
3984
  if (also === void 0) {
3689
3985
  return void 0;
3690
3986
  }
3691
- const file = (0, import_node_path6.join)(root, ...also.file.split("/"));
3692
- if ((0, import_node_fs3.existsSync)(file)) {
3987
+ const file = (0, import_node_path8.join)(root, ...also.file.split("/"));
3988
+ if ((0, import_node_fs6.existsSync)(file)) {
3693
3989
  return also.ifPresent;
3694
3990
  }
3695
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3696
- (0, import_node_fs3.writeFileSync)(file, also.content, "utf8");
3991
+ (0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
3992
+ (0, import_node_fs6.writeFileSync)(file, also.content, "utf8");
3697
3993
  return `Wrote ${also.file} to register it.`;
3698
3994
  }
3699
3995
  function withWarning(note, warning) {
@@ -3706,8 +4002,15 @@ function withWarning(note, warning) {
3706
4002
  function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3707
4003
  const steps = [
3708
4004
  ensurePenvDir(root),
3709
- writeSchemaFile(root, fields, draft, decisions),
4005
+ // The shape at the root, then the wrapper that loads it — both write-once.
4006
+ // The shape step needs the decisions too: an old-layout `schemaFile` already
4007
+ // holds the shape, and a second `penv.schema.ts` beside it would not compile.
4008
+ writeSchemaShapeFile(root, fields, draft, decisions),
4009
+ writeEnvFile(root, decisions),
3710
4010
  writeConfigFile(root, decisions),
4011
+ // After the config, whose evaluated shape the snapshot embeds; the wrapper
4012
+ // env.ts written above already imports it.
4013
+ writeSnapshotModule(root, decisions),
3711
4014
  writeTsconfigAlias(root, decisions),
3712
4015
  writeGitignore(root, decisions)
3713
4016
  ];
@@ -3715,7 +4018,7 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
3715
4018
  return seam === void 0 ? steps : [...steps, seam];
3716
4019
  }
3717
4020
  function runInit(options) {
3718
- const root = (0, import_node_path6.resolve)(options.cwd);
4021
+ const root = (0, import_node_path8.resolve)(options.cwd);
3719
4022
  const decisions = options.decisions ?? planInit(root).decisions;
3720
4023
  return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
3721
4024
  }
@@ -3724,10 +4027,12 @@ function renderInit(result2) {
3724
4027
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
3725
4028
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
3726
4029
  });
4030
+ const schemaStep = result2.steps.find((step) => step.target === "schema");
4031
+ const shapeFile = schemaStep?.action === "conflicted" ? result2.decisions.schemaFile : import_core16.SCHEMA_SHAPE_FILE;
3727
4032
  return [
3728
4033
  ...formatSteps(steps),
3729
4034
  "",
3730
- `${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${result2.decisions.schemaFile}, then:`,
4035
+ `${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${shapeFile}, then:`,
3731
4036
  tip(out.cyan("penv set <key>")),
3732
4037
  ...result2.decisions.environments.length === 0 ? [
3733
4038
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
@@ -3755,7 +4060,7 @@ var initCommand = (0, import_citty9.defineCommand)({
3755
4060
  },
3756
4061
  schema: {
3757
4062
  type: "string",
3758
- description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core15.DEFAULT_SCHEMA_FILE})`
4063
+ description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core16.DEFAULT_SCHEMA_FILE})`
3759
4064
  },
3760
4065
  alias: {
3761
4066
  type: "string",
@@ -3768,7 +4073,7 @@ var initCommand = (0, import_citty9.defineCommand)({
3768
4073
  },
3769
4074
  run({ args }) {
3770
4075
  return guard(async () => {
3771
- const root = (0, import_node_path6.resolve)(process.cwd());
4076
+ const root = (0, import_node_path8.resolve)(process.cwd());
3772
4077
  const environments = environmentsFromFlag(args.env);
3773
4078
  const plan2 = planInit(root, {
3774
4079
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -3817,7 +4122,7 @@ function inferType(value) {
3817
4122
  }
3818
4123
  function draftFields(entries) {
3819
4124
  return entries.map((entry) => {
3820
- const key = (0, import_core16.accessPath)((0, import_core16.refFromVariable)(entry.key)).join(".");
4125
+ const key = (0, import_core17.accessPath)((0, import_core17.refFromVariable)(entry.key)).join(".");
3821
4126
  return {
3822
4127
  key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
3823
4128
  type: inferType(entry.value)
@@ -3828,33 +4133,33 @@ function assertImportable(ref, variable) {
3828
4133
  if (!ref.name.includes(".")) {
3829
4134
  return;
3830
4135
  }
3831
- throw new import_core16.PenvError(
4136
+ throw new import_core17.PenvError(
3832
4137
  "IMPORT_UNPARSEABLE_NAME",
3833
4138
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
3834
4139
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
3835
4140
  );
3836
4141
  }
3837
4142
  function assertNotReserved(ref, variable, where, config) {
3838
- if ((0, import_core16.isReservedToken)(ref.name, config)) {
3839
- throw new import_core16.ReservedTokenError("parameter", variable, where);
4143
+ if ((0, import_core17.isReservedToken)(ref.name, config)) {
4144
+ throw new import_core17.ReservedTokenError("parameter", variable, where);
3840
4145
  }
3841
4146
  }
3842
4147
  function assertRoundTrips(ref, variable, config) {
3843
- if ((0, import_core16.roundTripsCleanly)(variable)) {
4148
+ if ((0, import_core17.roundTripsCleanly)(variable)) {
3844
4149
  return;
3845
4150
  }
3846
- if ((0, import_core16.variableName)(ref, config) === variable) {
4151
+ if ((0, import_core17.variableName)(ref, config) === variable) {
3847
4152
  return;
3848
4153
  }
3849
- const generated = (0, import_core16.variableName)(ref, config);
3850
- throw new import_core16.PenvError(
4154
+ const generated = (0, import_core17.variableName)(ref, config);
4155
+ throw new import_core17.PenvError(
3851
4156
  "IMPORT_LOSSY_NAME",
3852
4157
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
3853
4158
  `\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`override\` block of penv.config.ts \u2014 \`override: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
3854
4159
  );
3855
4160
  }
3856
4161
  function collisionsIn(refs, config) {
3857
- const errors = (0, import_core16.checkNameCollisions)(refs, config);
4162
+ const errors = (0, import_core17.checkNameCollisions)(refs, config);
3858
4163
  const first = errors[0];
3859
4164
  if (first !== void 0) {
3860
4165
  throw first;
@@ -3864,10 +4169,10 @@ function assertDeclared(segment, config) {
3864
4169
  if (config.environments.includes(segment)) {
3865
4170
  return segment;
3866
4171
  }
3867
- throw new import_core16.UnknownEnvironmentError(segment, config.environments);
4172
+ throw new import_core17.UnknownEnvironmentError(segment, config.environments);
3868
4173
  }
3869
4174
  function scopeFromFilename(file, config) {
3870
- const name = (0, import_node_path7.basename)(file);
4175
+ const name = (0, import_node_path9.basename)(file);
3871
4176
  const segments = name.split(".");
3872
4177
  const start = segments.indexOf(DOTENV_SEGMENT);
3873
4178
  if (start === -1) {
@@ -3889,13 +4194,13 @@ function scopeFromFilename(file, config) {
3889
4194
  return { kind: "environment-local", environment: assertDeclared(first, config) };
3890
4195
  }
3891
4196
  if (first === LOCAL && rest.length === 2) {
3892
- throw new import_core16.FilenameGrammarError(
4197
+ throw new import_core17.FilenameGrammarError(
3893
4198
  name,
3894
4199
  "`local` precedes the environment segment",
3895
4200
  `The environment segment always precedes \`local\` \u2014 \`.env.${second}.${LOCAL}\` is the file Next.js and Vite read, and \`.env.${LOCAL}.${second}\` is not a synonym for it. Rename it to \`.env.${second}.${LOCAL}\`, then import it again. Nothing was imported.`
3896
4201
  );
3897
4202
  }
3898
- throw new import_core16.FilenameGrammarError(
4203
+ throw new import_core17.FilenameGrammarError(
3899
4204
  name,
3900
4205
  `\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
3901
4206
  "A dotenv file carries exactly one scope: `.env`, `.env.<environment>`, `.env.local`, or `.env.<environment>.local`. Point `penv import` at one of those. Nothing was imported."
@@ -3910,7 +4215,7 @@ function environmentOf(scope) {
3910
4215
  case "local":
3911
4216
  return void 0;
3912
4217
  default:
3913
- return (0, import_core16.assertNever)(scope, "scope");
4218
+ return (0, import_core17.assertNever)(scope, "scope");
3914
4219
  }
3915
4220
  }
3916
4221
  function scopeWithEnvironment(scope, environment) {
@@ -3923,7 +4228,7 @@ function scopeWithEnvironment(scope, environment) {
3923
4228
  case "environment-local":
3924
4229
  return scope;
3925
4230
  default:
3926
- return (0, import_core16.assertNever)(scope, "scope");
4231
+ return (0, import_core17.assertNever)(scope, "scope");
3927
4232
  }
3928
4233
  }
3929
4234
  function explicitEnvironment(options, source, config) {
@@ -3931,7 +4236,7 @@ function explicitEnvironment(options, source, config) {
3931
4236
  if (value.length > 0) {
3932
4237
  return value;
3933
4238
  }
3934
- throw new import_core16.PenvError(
4239
+ throw new import_core17.PenvError(
3935
4240
  "IMPORT_ENV_FLAG_EMPTY",
3936
4241
  `\`--env\` for the import of ${source} names no environment`,
3937
4242
  `Pass a declared environment \u2014 ${config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
@@ -3941,7 +4246,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
3941
4246
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
3942
4247
  return;
3943
4248
  }
3944
- throw new import_core16.PenvError(
4249
+ throw new import_core17.PenvError(
3945
4250
  "IMPORT_ENV_CONFLICT",
3946
4251
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
3947
4252
  `Drop \`--env\` to import ${source} as ${derived}, pass \`--env ${derived}\` to say the same thing twice, or point \`penv import\` at the file that holds ${explicit}'s values. Nothing was imported.`
@@ -3951,7 +4256,7 @@ function environmentNamed(file, explicit) {
3951
4256
  if (explicit !== void 0 && explicit.trim().length > 0) {
3952
4257
  return explicit.trim();
3953
4258
  }
3954
- const segments = (0, import_node_path7.basename)(file).split(".");
4259
+ const segments = (0, import_node_path9.basename)(file).split(".");
3955
4260
  const start = segments.indexOf(DOTENV_SEGMENT);
3956
4261
  if (start === -1) {
3957
4262
  return void 0;
@@ -3965,15 +4270,15 @@ function decisionsOf(config, cwd) {
3965
4270
  // `import` re-scaffolds env.ts for an existing project; injection is an init
3966
4271
  // choice, so it is not turned on here.
3967
4272
  inject: false,
3968
- schemaFile: (0, import_core16.schemaFileOf)(config),
4273
+ schemaFile: (0, import_core17.schemaFileOf)(config),
3969
4274
  publicPrefixes: config.publicPrefixes ?? [],
3970
4275
  alias: detectAlias(cwd)
3971
4276
  };
3972
4277
  }
3973
4278
  function configInEffect(cwd, environment) {
3974
- const existing = (0, import_core16.findConfigFile)(cwd);
4279
+ const existing = (0, import_core17.findConfigFile)(cwd);
3975
4280
  if (existing !== void 0) {
3976
- const config = (0, import_core16.loadConfigFrom)(existing);
4281
+ const config = (0, import_core17.loadConfigFrom)(existing);
3977
4282
  return { config, decisions: decisionsOf(config, cwd) };
3978
4283
  }
3979
4284
  const planned = planInit(cwd).decisions;
@@ -3985,16 +4290,16 @@ function configInEffect(cwd, environment) {
3985
4290
  return { config: openProject(cwd).config, decisions };
3986
4291
  }
3987
4292
  function importDotenv(options) {
3988
- const cwd = (0, import_node_path7.resolve)(options.cwd);
3989
- const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
3990
- if (!(0, import_node_fs4.existsSync)(file)) {
3991
- throw new import_core16.PenvError(
4293
+ const cwd = (0, import_node_path9.resolve)(options.cwd);
4294
+ const file = (0, import_node_path9.isAbsolute)(options.file) ? options.file : (0, import_node_path9.resolve)(cwd, options.file);
4295
+ if (!(0, import_node_fs7.existsSync)(file)) {
4296
+ throw new import_core17.PenvError(
3992
4297
  "IMPORT_FILE_MISSING",
3993
4298
  `There is no file at ${file} to import`,
3994
4299
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
3995
4300
  );
3996
4301
  }
3997
- const parsed = (0, import_core16.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
4302
+ const parsed = (0, import_core17.parseDotenv)((0, import_node_fs7.readFileSync)(file, "utf8"));
3998
4303
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
3999
4304
  const source = displayPath2(cwd, file);
4000
4305
  const named = scopeFromFilename(file, config);
@@ -4002,10 +4307,10 @@ function importDotenv(options) {
4002
4307
  const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
4003
4308
  assertEnvironmentAgrees(derived, explicit, source);
4004
4309
  const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
4005
- const environment = (0, import_core16.lookupEnvironment)(config, explicit ?? derived);
4310
+ const environment = (0, import_core17.lookupEnvironment)(config, explicit ?? derived);
4006
4311
  const refs = [];
4007
4312
  for (const entry of parsed.entries) {
4008
- const ref = (0, import_core16.refFromVariable)(entry.key);
4313
+ const ref = (0, import_core17.refFromVariable)(entry.key);
4009
4314
  assertImportable(ref, entry.key);
4010
4315
  assertNotReserved(ref, entry.key, source, config);
4011
4316
  assertRoundTrips(ref, entry.key, config);
@@ -4031,7 +4336,8 @@ function importDotenv(options) {
4031
4336
  }
4032
4337
  }
4033
4338
  const backup = `${file}${BACKUP_SUFFIX}`;
4034
- (0, import_node_fs4.copyFileSync)(file, backup);
4339
+ (0, import_node_fs7.copyFileSync)(file, backup);
4340
+ refreshSnapshot(project);
4035
4341
  return {
4036
4342
  root: project.root,
4037
4343
  file,
@@ -4045,7 +4351,7 @@ function importDotenv(options) {
4045
4351
  };
4046
4352
  }
4047
4353
  function displayPath2(root, file) {
4048
- const rel = (0, import_node_path7.relative)(root, file);
4354
+ const rel = (0, import_node_path9.relative)(root, file);
4049
4355
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
4050
4356
  }
4051
4357
  function keptSchemaStep(step, variables) {
@@ -4133,7 +4439,7 @@ var importCommand = (0, import_citty10.defineCommand)({
4133
4439
 
4134
4440
  // src/commands/key.ts
4135
4441
  var import_node_crypto = require("crypto");
4136
- var import_core17 = require("@penvhq/core");
4442
+ var import_core18 = require("@penvhq/core");
4137
4443
  var import_citty11 = require("citty");
4138
4444
 
4139
4445
  // src/keychain.ts
@@ -4167,35 +4473,35 @@ function runKeyCreate(options) {
4167
4473
  const environment = targetEnvironment(project, options.environment);
4168
4474
  const declared = project.config.keys?.[environment];
4169
4475
  if (declared === void 0) {
4170
- throw new import_core17.PenvError(
4476
+ throw new import_core18.PenvError(
4171
4477
  "KEY_SOURCE_UNDECLARED",
4172
4478
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
4173
4479
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
4174
4480
  );
4175
4481
  }
4176
- const key = (0, import_node_crypto.randomBytes)(import_core17.KEY_BYTES).toString("base64");
4482
+ const key = (0, import_node_crypto.randomBytes)(import_core18.KEY_BYTES).toString("base64");
4177
4483
  if (declared.source === "keychain") {
4178
4484
  const keychain = options.keychain ?? defaultKeychain;
4179
4485
  if (options.force !== true) {
4180
4486
  let existing;
4181
4487
  try {
4182
- existing = keychain.getPassword(import_core17.KEYCHAIN_SERVICE, declared.id);
4488
+ existing = keychain.getPassword(import_core18.KEYCHAIN_SERVICE, declared.id);
4183
4489
  } catch (cause) {
4184
- throw new import_core17.PenvError(
4490
+ throw new import_core18.PenvError(
4185
4491
  "KEYCHAIN_UNAVAILABLE",
4186
4492
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
4187
4493
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
4188
4494
  );
4189
4495
  }
4190
4496
  if (existing !== null) {
4191
- throw new import_core17.PenvError(
4497
+ throw new import_core18.PenvError(
4192
4498
  "KEY_EXISTS",
4193
4499
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
4194
4500
  "Replacing it would orphan every value already sealed under it \u2014 they could never be decrypted again. Re-run with `--force` only if you are certain nothing is sealed under the current key."
4195
4501
  );
4196
4502
  }
4197
4503
  }
4198
- keychain.setPassword(import_core17.KEYCHAIN_SERVICE, declared.id, key);
4504
+ keychain.setPassword(import_core18.KEYCHAIN_SERVICE, declared.id, key);
4199
4505
  return { source: "keychain", environment, id: declared.id };
4200
4506
  }
4201
4507
  return {
@@ -4263,7 +4569,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
4263
4569
  });
4264
4570
 
4265
4571
  // src/commands/list.ts
4266
- var import_core18 = require("@penvhq/core");
4572
+ var import_core19 = require("@penvhq/core");
4267
4573
  var import_citty12 = require("citty");
4268
4574
  function scopeLabel2(scope) {
4269
4575
  switch (scope.kind) {
@@ -4276,7 +4582,7 @@ function scopeLabel2(scope) {
4276
4582
  case "unscoped":
4277
4583
  return "default";
4278
4584
  default:
4279
- return (0, import_core18.assertNever)(scope, "scope");
4585
+ return (0, import_core19.assertNever)(scope, "scope");
4280
4586
  }
4281
4587
  }
4282
4588
  async function runList(options) {
@@ -4284,12 +4590,12 @@ async function runList(options) {
4284
4590
  const environment = targetEnvironment(project, options.environment);
4285
4591
  const keys = keySourceFor(project, environment);
4286
4592
  const parameters = [];
4287
- for (const resolution of await (0, import_core18.resolveAll)(environment, project.provider, keys)) {
4593
+ for (const resolution of await (0, import_core19.resolveAll)(environment, project.provider, keys)) {
4288
4594
  const winner = resolution.winner;
4289
4595
  const scope = winner?.file.scope;
4290
4596
  parameters.push({
4291
4597
  parameter: resolution.parameter,
4292
- variable: (0, import_core18.variableName)(resolution.ref, project.config),
4598
+ variable: (0, import_core19.variableName)(resolution.ref, project.config),
4293
4599
  scope: scope === void 0 ? "absent" : scopeLabel2(scope),
4294
4600
  location: winner?.location,
4295
4601
  encrypted: winner?.file.encrypted === true,
@@ -4342,7 +4648,7 @@ var listCommand = (0, import_citty12.defineCommand)({
4342
4648
  });
4343
4649
 
4344
4650
  // src/commands/mv.ts
4345
- var import_core19 = require("@penvhq/core");
4651
+ var import_core20 = require("@penvhq/core");
4346
4652
  var import_citty13 = require("citty");
4347
4653
  function environmentOf2(file) {
4348
4654
  const scope = file.scope;
@@ -4358,39 +4664,39 @@ async function planFile(project, source, target, parameter) {
4358
4664
  }
4359
4665
  const environment = environmentOf2(source);
4360
4666
  if (environment === void 0) {
4361
- throw new import_core19.PenvError(
4667
+ throw new import_core20.PenvError(
4362
4668
  "SECRET_SCOPE_AMBIGUOUS",
4363
- `${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
4669
+ `${PENV_DIR}/${(0, import_core20.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
4364
4670
  "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."
4365
4671
  );
4366
4672
  }
4367
4673
  const keys = keySourceFor(project, environment);
4368
- const opened = (0, import_core19.openValue)(source, stored, keys);
4674
+ const opened = (0, import_core20.openValue)(source, stored, keys);
4369
4675
  if (opened.kind === "failed") {
4370
- throw new import_core19.PenvError(
4676
+ throw new import_core20.PenvError(
4371
4677
  "VALUE_UNDECRYPTABLE",
4372
- `${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
4678
+ `${PENV_DIR}/${(0, import_core20.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
4373
4679
  "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."
4374
4680
  );
4375
4681
  }
4376
4682
  return {
4377
4683
  source,
4378
4684
  target,
4379
- contents: (0, import_core19.sealValue)(target, opened.value, keys, parameter, environment),
4685
+ contents: (0, import_core20.sealValue)(target, opened.value, keys, parameter, environment),
4380
4686
  resealed: true
4381
4687
  };
4382
4688
  }
4383
4689
  function filesOf(all, ref) {
4384
- const id = (0, import_core19.parameterId)(ref);
4385
- return all.filter((file) => (0, import_core19.parameterId)(file) === id);
4690
+ const id = (0, import_core20.parameterId)(ref);
4691
+ return all.filter((file) => (0, import_core20.parameterId)(file) === id);
4386
4692
  }
4387
4693
  async function runMove(options) {
4388
4694
  const project = openProject(options.cwd);
4389
4695
  const from = refFromKey(options.from, project.config);
4390
4696
  assertWritableKey(options.to);
4391
4697
  const to = refFromKey(options.to, project.config);
4392
- if ((0, import_core19.parameterId)(from) === (0, import_core19.parameterId)(to)) {
4393
- throw new import_core19.PenvError(
4698
+ if ((0, import_core20.parameterId)(from) === (0, import_core20.parameterId)(to)) {
4699
+ throw new import_core20.PenvError(
4394
4700
  "PARAMETER_UNCHANGED",
4395
4701
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
4396
4702
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -4400,24 +4706,24 @@ async function runMove(options) {
4400
4706
  const sources = filesOf(all, from);
4401
4707
  const meta = await project.provider.readMeta(from);
4402
4708
  if (sources.length === 0 && meta === void 0) {
4403
- throw new import_core19.PenvError(
4709
+ throw new import_core20.PenvError(
4404
4710
  "PARAMETER_ABSENT",
4405
- `Parameter ${(0, import_core19.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
4711
+ `Parameter ${(0, import_core20.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
4406
4712
  `\`penv list\` shows every parameter penv holds.`
4407
4713
  );
4408
4714
  }
4409
4715
  const occupied2 = filesOf(all, to);
4410
4716
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
4411
- throw new import_core19.PenvError(
4717
+ throw new import_core20.PenvError(
4412
4718
  "PARAMETER_EXISTS",
4413
- `Parameter ${(0, import_core19.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
4414
- `Remove or rename ${(0, import_core19.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
4719
+ `Parameter ${(0, import_core20.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
4720
+ `Remove or rename ${(0, import_core20.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
4415
4721
  );
4416
4722
  }
4417
4723
  const planned = [];
4418
4724
  for (const source of sources) {
4419
4725
  const target = { ...source, namespace: to.namespace, name: to.name };
4420
- const one = await planFile(project, source, target, (0, import_core19.parameterId)(to));
4726
+ const one = await planFile(project, source, target, (0, import_core20.parameterId)(to));
4421
4727
  if (one !== void 0) {
4422
4728
  planned.push(one);
4423
4729
  }
@@ -4434,19 +4740,21 @@ async function runMove(options) {
4434
4740
  if (meta !== void 0) {
4435
4741
  await project.provider.removeMeta(from);
4436
4742
  }
4743
+ refreshSnapshot(project);
4437
4744
  return {
4438
- from: (0, import_core19.parameterId)(from),
4439
- to: (0, import_core19.parameterId)(to),
4745
+ from: (0, import_core20.parameterId)(from),
4746
+ to: (0, import_core20.parameterId)(to),
4440
4747
  files: planned.map((file) => ({
4441
- from: (0, import_core19.formatValueFile)(file.source),
4442
- to: (0, import_core19.formatValueFile)(file.target),
4748
+ from: (0, import_core20.formatValueFile)(file.source),
4749
+ to: (0, import_core20.formatValueFile)(file.target),
4443
4750
  resealed: file.resealed
4444
4751
  })),
4445
4752
  // The meta's path, not the parameter's dotted id: `redis.password` is what
4446
4753
  // the schema calls it and `redis/password.json` is the file, and a report
4447
4754
  // that printed the first while moving the second names no file on disk.
4448
- meta: meta === void 0 ? void 0 : (0, import_core19.formatMetaFile)({ ...to, format: "json" }),
4449
- schema: { was: (0, import_core19.accessPath)(from).join("."), now: (0, import_core19.accessPath)(to).join(".") }
4755
+ meta: meta === void 0 ? void 0 : (0, import_core20.formatMetaFile)({ ...to, format: "json" }),
4756
+ schema: { was: (0, import_core20.accessPath)(from).join("."), now: (0, import_core20.accessPath)(to).join(".") },
4757
+ schemaFile: schemaShapeFileOf(project)
4450
4758
  };
4451
4759
  }
4452
4760
  function renderMove(result2) {
@@ -4463,7 +4771,7 @@ function renderMove(result2) {
4463
4771
  lines.push(
4464
4772
  "",
4465
4773
  tip(
4466
- `.penv/env.ts still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
4774
+ `${result2.schemaFile} still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
4467
4775
  )
4468
4776
  );
4469
4777
  return lines;
@@ -4490,7 +4798,7 @@ var mvCommand = (0, import_citty13.defineCommand)({
4490
4798
  });
4491
4799
 
4492
4800
  // src/commands/pull.ts
4493
- var import_core20 = require("@penvhq/core");
4801
+ var import_core21 = require("@penvhq/core");
4494
4802
  var import_citty14 = require("citty");
4495
4803
  async function pullProjection(project, source, environment) {
4496
4804
  const tree = localTree(project);
@@ -4503,7 +4811,7 @@ async function pullProjection(project, source, environment) {
4503
4811
  }
4504
4812
  let meta = 0;
4505
4813
  for (const name of [...names].sort()) {
4506
- const ref = (0, import_core20.refFromVariable)(name);
4814
+ const ref = (0, import_core21.refFromVariable)(name);
4507
4815
  if (tree.readMetaSync(ref) === void 0) {
4508
4816
  tree.writeMetaSync(ref, {});
4509
4817
  meta += 1;
@@ -4526,7 +4834,7 @@ async function runPull(options) {
4526
4834
  if (source.type === LOCAL_TREE_TYPE) {
4527
4835
  return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
4528
4836
  }
4529
- if ((0, import_core20.holdsProjection)(source)) {
4837
+ if ((0, import_core21.holdsProjection)(source)) {
4530
4838
  return pullProjection(project, source, environment);
4531
4839
  }
4532
4840
  const tree = localTree(project);
@@ -4550,6 +4858,7 @@ async function runPull(options) {
4550
4858
  tree.writeMetaSync(ref, block);
4551
4859
  meta += 1;
4552
4860
  }
4861
+ refreshSnapshot(project);
4553
4862
  return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
4554
4863
  }
4555
4864
  function renderPull(result2) {
@@ -4618,7 +4927,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
4618
4927
  });
4619
4928
 
4620
4929
  // src/commands/remove.ts
4621
- var import_core21 = require("@penvhq/core");
4930
+ var import_core22 = require("@penvhq/core");
4622
4931
  var import_citty15 = require("citty");
4623
4932
  async function runRemove(options) {
4624
4933
  const project = openProject(options.cwd);
@@ -4636,12 +4945,13 @@ async function runRemove(options) {
4636
4945
  continue;
4637
4946
  }
4638
4947
  await project.provider.remove(file);
4639
- removed.push((0, import_core21.formatValueFile)(file));
4948
+ removed.push((0, import_core22.formatValueFile)(file));
4640
4949
  }
4950
+ refreshSnapshot(project);
4641
4951
  return {
4642
4952
  parameter: options.key,
4643
4953
  removed,
4644
- considered: files.map((file) => (0, import_core21.formatValueFile)(file))
4954
+ considered: files.map((file) => (0, import_core22.formatValueFile)(file))
4645
4955
  };
4646
4956
  }
4647
4957
  function renderRemove(result2) {
@@ -4690,7 +5000,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
4690
5000
  });
4691
5001
 
4692
5002
  // src/commands/rotate.ts
4693
- var import_core22 = require("@penvhq/core");
5003
+ var import_core23 = require("@penvhq/core");
4694
5004
  var import_citty16 = require("citty");
4695
5005
  function rotatingFile(ref, environment) {
4696
5006
  return {
@@ -4710,13 +5020,14 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
4710
5020
  value,
4711
5021
  environment
4712
5022
  });
5023
+ refreshSnapshot(project);
4713
5024
  return;
4714
5025
  }
4715
5026
  await provider.write(rotatingFile(ref, environment), value);
4716
5027
  }
4717
5028
  function requireNewValue(value, phase, key) {
4718
5029
  if (value === void 0) {
4719
- throw new import_core22.PenvError(
5030
+ throw new import_core23.PenvError(
4720
5031
  "ROTATION_NO_VALUE",
4721
5032
  `A ${phase} rotation of ${key} writes a new value, and none was given`,
4722
5033
  "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
@@ -4725,8 +5036,8 @@ function requireNewValue(value, phase, key) {
4725
5036
  return value;
4726
5037
  }
4727
5038
  function requireRetaining(provider, environment) {
4728
- if (!(0, import_core22.retainsPrevious)(provider)) {
4729
- throw new import_core22.PenvError(
5039
+ if (!(0, import_core23.retainsPrevious)(provider)) {
5040
+ throw new import_core23.PenvError(
4730
5041
  "ROTATION_NOT_RETAINING",
4731
5042
  `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`,
4732
5043
  "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."
@@ -4739,8 +5050,8 @@ async function runRotate(options) {
4739
5050
  const environment = targetEnvironment(project, options.environment);
4740
5051
  const ref = refFromKey(options.key, project.config);
4741
5052
  const source = await sourceProviderFor(project, environment);
4742
- if (!(0, import_core22.holdsRecords)(source)) {
4743
- throw new import_core22.PenvError(
5053
+ if (!(0, import_core23.holdsRecords)(source)) {
5054
+ throw new import_core23.PenvError(
4744
5055
  "ROTATION_NOT_RECORDS",
4745
5056
  `Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
4746
5057
  "Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
@@ -4749,9 +5060,9 @@ async function runRotate(options) {
4749
5060
  const provider = source;
4750
5061
  const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
4751
5062
  const before = await provider.readMeta(ref);
4752
- const { mechanism } = (0, import_core22.rotationOf)(before, environment);
5063
+ const { mechanism } = (0, import_core23.rotationOf)(before, environment);
4753
5064
  if (mechanism === void 0) {
4754
- throw new import_core22.PenvError(
5065
+ throw new import_core23.PenvError(
4755
5066
  "ROTATION_NO_MECHANISM",
4756
5067
  `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
4757
5068
  '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.'
@@ -4761,7 +5072,7 @@ async function runRotate(options) {
4761
5072
  const complete = options.complete === true;
4762
5073
  if (mechanism === "atomic-cutover") {
4763
5074
  if (begin || complete) {
4764
- throw new import_core22.PenvError(
5075
+ throw new import_core23.PenvError(
4765
5076
  "ROTATION_MECHANISM_MISMATCH",
4766
5077
  `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
4767
5078
  "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."
@@ -4769,13 +5080,13 @@ async function runRotate(options) {
4769
5080
  }
4770
5081
  const value = requireNewValue(options.value, "cutover", options.key);
4771
5082
  await writeRotatedValue(project, provider, ref, environment, value);
4772
- const after2 = (0, import_core22.completeRotation)(before, environment, nowIso);
5083
+ const after2 = (0, import_core23.completeRotation)(before, environment, nowIso);
4773
5084
  await provider.writeMeta(ref, after2);
4774
5085
  return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
4775
5086
  }
4776
5087
  const retaining = requireRetaining(provider, environment);
4777
5088
  if (begin === complete) {
4778
- throw new import_core22.PenvError(
5089
+ throw new import_core23.PenvError(
4779
5090
  "ROTATION_PHASE_REQUIRED",
4780
5091
  `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
4781
5092
  "`--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."
@@ -4784,16 +5095,16 @@ async function runRotate(options) {
4784
5095
  if (begin) {
4785
5096
  const value = requireNewValue(options.value, "begin", options.key);
4786
5097
  await writeRotatedValue(project, retaining, ref, environment, value);
4787
- const after2 = (0, import_core22.beginRotation)(before, environment, nowIso);
5098
+ const after2 = (0, import_core23.beginRotation)(before, environment, nowIso);
4788
5099
  await retaining.writeMeta(ref, after2);
4789
5100
  return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
4790
5101
  }
4791
- const after = (0, import_core22.completeRotation)(before, environment, nowIso);
5102
+ const after = (0, import_core23.completeRotation)(before, environment, nowIso);
4792
5103
  await retaining.writeMeta(ref, after);
4793
5104
  return result(ref, environment, mechanism, "complete", retaining.type, false, after);
4794
5105
  }
4795
5106
  function result(ref, environment, mechanism, phase, source, wroteValue, after) {
4796
- const { state, rotatingSince, lastRotated } = (0, import_core22.rotationOf)(after, environment);
5107
+ const { state, rotatingSince, lastRotated } = (0, import_core23.rotationOf)(after, environment);
4797
5108
  return {
4798
5109
  parameter: [...ref.namespace, ref.name].join("/"),
4799
5110
  environment,
@@ -4877,15 +5188,57 @@ var rotateCommand = (0, import_citty16.defineCommand)({
4877
5188
  }
4878
5189
  });
4879
5190
 
4880
- // src/commands/watch.ts
4881
- var import_node_fs5 = require("fs");
4882
- var import_node_path8 = require("path");
4883
- var import_core23 = require("@penvhq/core");
5191
+ // src/commands/snapshot.ts
4884
5192
  var import_citty17 = require("citty");
5193
+ function runSnapshot(options) {
5194
+ const project = openProject(options.cwd);
5195
+ return { write: writeSnapshotFile(project), wire: wireEnvModule(project) };
5196
+ }
5197
+ function renderSnapshot(result2) {
5198
+ const label = result2.write.action === "created" ? "Generated" : result2.write.action === "updated" ? "Updated" : "Up to date";
5199
+ const rows = [{ glyph: CHECK, label, subject: SNAPSHOT_FILE }];
5200
+ if (result2.wire.action === "wired") {
5201
+ rows.push({
5202
+ glyph: CHECK,
5203
+ label: "Wired",
5204
+ subject: result2.wire.file,
5205
+ detail: "load reads the snapshot"
5206
+ });
5207
+ } else if (result2.wire.action === "kept") {
5208
+ rows.push({ glyph: CHECK, label: "Wired", subject: result2.wire.file });
5209
+ }
5210
+ const lines = formatRows(rows);
5211
+ if (result2.wire.action === "manual") {
5212
+ lines.push(
5213
+ "",
5214
+ `${WARN} penv could not wire ${result2.wire.file} automatically \u2014 add these two lines yourself:`,
5215
+ tip(result2.wire.importLine),
5216
+ tip(result2.wire.loadHint)
5217
+ );
5218
+ }
5219
+ return lines;
5220
+ }
5221
+ var snapshotCommand = (0, import_citty17.defineCommand)({
5222
+ meta: {
5223
+ name: "snapshot",
5224
+ description: "Generate penv.snapshot.ts so load() resolves in a bundled/serverless runtime"
5225
+ },
5226
+ run() {
5227
+ return guard(async () => {
5228
+ write(renderSnapshot(runSnapshot({ cwd: process.cwd() })));
5229
+ });
5230
+ }
5231
+ });
5232
+
5233
+ // src/commands/watch.ts
5234
+ var import_node_fs8 = require("fs");
5235
+ var import_node_path10 = require("path");
5236
+ var import_core24 = require("@penvhq/core");
5237
+ var import_citty18 = require("citty");
4885
5238
  var DEBOUNCE_MS = 100;
4886
5239
  function runWatch(options) {
4887
5240
  const project = openProject(options.cwd);
4888
- const configFile = (0, import_node_path8.basename)(project.configFile);
5241
+ const configFile = (0, import_node_path10.basename)(project.configFile);
4889
5242
  const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
4890
5243
  const watchers = /* @__PURE__ */ new Set();
4891
5244
  let timer;
@@ -4938,22 +5291,22 @@ function runWatch(options) {
4938
5291
  watcher.close();
4939
5292
  }
4940
5293
  function armRecovery(target, recursive, only) {
4941
- const parent = (0, import_node_path8.dirname)(target);
4942
- const name = (0, import_node_path8.basename)(target);
5294
+ const parent = (0, import_node_path10.dirname)(target);
5295
+ const name = (0, import_node_path10.basename)(target);
4943
5296
  let recovery;
4944
5297
  try {
4945
- recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
5298
+ recovery = (0, import_node_fs8.watch)(parent, { recursive: false }, (_event, filename) => {
4946
5299
  if (closed || recovery === void 0) {
4947
5300
  return;
4948
5301
  }
4949
- if (!(0, import_node_fs5.existsSync)(parent)) {
5302
+ if (!(0, import_node_fs8.existsSync)(parent)) {
4950
5303
  stop(recovery);
4951
5304
  return;
4952
5305
  }
4953
- if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
5306
+ if (filename !== null && (0, import_node_path10.basename)(filename) !== name) {
4954
5307
  return;
4955
5308
  }
4956
- if (!(0, import_node_fs5.existsSync)(target)) {
5309
+ if (!(0, import_node_fs8.existsSync)(target)) {
4957
5310
  return;
4958
5311
  }
4959
5312
  stop(recovery);
@@ -4973,11 +5326,11 @@ function runWatch(options) {
4973
5326
  }
4974
5327
  function addWatcher(target, recursive, only) {
4975
5328
  let watcher;
4976
- const listen = (useRecursive) => (0, import_node_fs5.watch)(target, { recursive: useRecursive }, (_event, filename) => {
5329
+ const listen = (useRecursive) => (0, import_node_fs8.watch)(target, { recursive: useRecursive }, (_event, filename) => {
4977
5330
  if (closed) {
4978
5331
  return;
4979
5332
  }
4980
- if (!(0, import_node_fs5.existsSync)(target)) {
5333
+ if (!(0, import_node_fs8.existsSync)(target)) {
4981
5334
  if (watcher !== void 0) {
4982
5335
  stop(watcher);
4983
5336
  }
@@ -4985,7 +5338,7 @@ function runWatch(options) {
4985
5338
  schedule();
4986
5339
  return;
4987
5340
  }
4988
- if (only !== void 0 && (filename === null || (0, import_node_path8.basename)(filename) !== only)) {
5341
+ if (only !== void 0 && (filename === null || (0, import_node_path10.basename)(filename) !== only)) {
4989
5342
  return;
4990
5343
  }
4991
5344
  schedule();
@@ -5012,10 +5365,11 @@ function runWatch(options) {
5012
5365
  watchers.add(watcher);
5013
5366
  }
5014
5367
  addWatcher(project.penvDir, true);
5015
- addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
5016
- if ((0, import_core23.schemaInsideTree)(project.config) === void 0) {
5017
- const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core23.schemaFileOf)(project.config));
5018
- addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
5368
+ addWatcher((0, import_node_path10.dirname)(project.configFile), false, configFile);
5369
+ addWatcher(project.root, false, import_core24.SCHEMA_SHAPE_FILE);
5370
+ if ((0, import_core24.schemaInsideTree)(project.config) === void 0) {
5371
+ const schemaFile = (0, import_node_path10.resolve)(project.root, (0, import_core24.schemaFileOf)(project.config));
5372
+ addWatcher((0, import_node_path10.dirname)(schemaFile), false, (0, import_node_path10.basename)(schemaFile));
5019
5373
  }
5020
5374
  void validate();
5021
5375
  return {
@@ -5058,7 +5412,7 @@ function renderDrift(drift, environment) {
5058
5412
  }
5059
5413
  return lines;
5060
5414
  }
5061
- var watchCommand = (0, import_citty17.defineCommand)({
5415
+ var watchCommand = (0, import_citty18.defineCommand)({
5062
5416
  meta: {
5063
5417
  name: "watch",
5064
5418
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -5097,7 +5451,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
5097
5451
  });
5098
5452
 
5099
5453
  // src/index.ts
5100
- var main = (0, import_citty18.defineCommand)({
5454
+ var main = (0, import_citty19.defineCommand)({
5101
5455
  meta: {
5102
5456
  name: "penv",
5103
5457
  description: "Configuration that shares a data model with your production secret manager"
@@ -5118,14 +5472,15 @@ var main = (0, import_citty18.defineCommand)({
5118
5472
  encrypt: encryptCommand,
5119
5473
  decrypt: decryptCommand,
5120
5474
  key: keyCommand,
5475
+ snapshot: snapshotCommand,
5121
5476
  validate: validateCommand,
5122
5477
  doctor: doctorCommand,
5123
5478
  watch: watchCommand
5124
5479
  }
5125
5480
  });
5126
5481
  function runMain() {
5127
- (0, import_core24.setKeychain)(defaultKeychain);
5128
- return (0, import_citty18.runMain)(main);
5482
+ (0, import_core25.setKeychain)(defaultKeychain);
5483
+ return (0, import_citty19.runMain)(main);
5129
5484
  }
5130
5485
  // Annotate the CommonJS export names for ESM import in node:
5131
5486
  0 && (module.exports = {
@@ -5140,6 +5495,7 @@ function runMain() {
5140
5495
  renderPull,
5141
5496
  renderPush,
5142
5497
  renderRotate,
5498
+ renderSnapshot,
5143
5499
  renderWatch,
5144
5500
  runDecrypt,
5145
5501
  runDoctor,
@@ -5157,6 +5513,7 @@ function runMain() {
5157
5513
  runRemove,
5158
5514
  runRotate,
5159
5515
  runSet,
5516
+ runSnapshot,
5160
5517
  runValidate,
5161
5518
  runWatch
5162
5519
  });