@penvhq/cli 0.6.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,11 +3023,11 @@ 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
- const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
3030
+ const dir = srcPrefix(cwd);
2807
3031
  const preferred = `${dir}env.ts`;
2808
3032
  if (!occupied(cwd, preferred)) {
2809
3033
  return { file: preferred };
@@ -2812,7 +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 };
3040
+ }
3041
+ function srcPrefix(cwd) {
3042
+ return (0, import_node_fs5.existsSync)((0, import_node_path7.join)(cwd, "src")) ? "src/" : "";
2816
3043
  }
2817
3044
  function dependenciesOf(cwd) {
2818
3045
  const manifest = manifestOf(cwd);
@@ -2831,13 +3058,13 @@ function dependenciesOf(cwd) {
2831
3058
  return names;
2832
3059
  }
2833
3060
  function manifestOf(cwd) {
2834
- const file = (0, import_node_path5.join)(cwd, "package.json");
2835
- 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)) {
2836
3063
  return void 0;
2837
3064
  }
2838
3065
  let manifest;
2839
3066
  try {
2840
- manifest = JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
3067
+ manifest = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
2841
3068
  } catch {
2842
3069
  return void 0;
2843
3070
  }
@@ -2853,7 +3080,7 @@ function detectFramework(cwd) {
2853
3080
  return detectedFrom(cwd, signature.name, signature.publicPrefixes);
2854
3081
  }
2855
3082
  }
2856
- 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")) {
2857
3084
  return detectedFrom(cwd, "Bun", []);
2858
3085
  }
2859
3086
  return void 0;
@@ -2879,10 +3106,10 @@ function hasImportsBlock(cwd) {
2879
3106
  }
2880
3107
 
2881
3108
  // src/commands/init.ts
2882
- var import_node_fs3 = require("fs");
2883
- var import_node_path6 = require("path");
3109
+ var import_node_fs6 = require("fs");
3110
+ var import_node_path8 = require("path");
2884
3111
  var import_promises = require("readline/promises");
2885
- var import_core15 = require("@penvhq/core");
3112
+ var import_core16 = require("@penvhq/core");
2886
3113
  var import_citty9 = require("citty");
2887
3114
 
2888
3115
  // src/seams.ts
@@ -2951,19 +3178,25 @@ function bun({ alias }) {
2951
3178
  kind: "scaffold",
2952
3179
  file: ".penv/preload.ts",
2953
3180
  content: `// Bun evaluates this file before your entry point, because it is listed under
2954
- // \`preload\` in bunfig.toml (see the note penv printed). Running it injects your
2955
- // config into process.env before any library reads it.
3181
+ // \`preload\` in bunfig.toml. Running it injects your config into process.env
3182
+ // before any library reads it.
2956
3183
  import "${alias}";
2957
3184
  `,
2958
3185
  ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
2959
- notes: [
2960
- 'Register it in bunfig.toml so Bun runs it: `preload = ["./.penv/preload.ts"]` (and the same under `[test]`).',
2961
- `\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`
2962
- ]
3186
+ notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
3187
+ // The preload file is inert until bunfig.toml points at it — so penv writes
3188
+ // that too, both for the runtime and for `bun test`. Only when there is no
3189
+ // bunfig to overwrite; an existing one is the user's, and penv prints instead.
3190
+ also: {
3191
+ file: "bunfig.toml",
3192
+ content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
3193
+ ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
3194
+ }
2963
3195
  };
2964
3196
  }
2965
- function node({ alias, srcDir }) {
2966
- const runtimeSpecifier = alias.startsWith("#") ? alias : `./${srcDir}.penv/env.js`;
3197
+ function node({ alias, schemaFile }) {
3198
+ const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
3199
+ const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
2967
3200
  return {
2968
3201
  kind: "instruct",
2969
3202
  instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
@@ -2983,10 +3216,17 @@ import before it. If you have no server.ts yet, create it from TanStack's server
2983
3216
  template and put that import first.`
2984
3217
  };
2985
3218
  }
2986
- function astro(_context) {
3219
+ function astro({ alias }) {
2987
3220
  return {
2988
3221
  kind: "instruct",
2989
- instruction: 'Astro has no single pre-app hook. Add a small integration to astro.config that\ninjects the import into every SSR page, then cover the routes it cannot reach:\n \u2022 In astro.config integrations, add:\n { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>\n injectScript("page-ssr", \'import "@env";\') } }\n \u2022 Also add `import "@env";` as the first line of src/middleware.ts and of any\n endpoint route (src/pages/*.ts) that imports a library reading process.env.\nInjection only applies under a server (on-demand) adapter, not a static build.'
3222
+ instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
3223
+ injects the import into every SSR page, then cover the routes it cannot reach:
3224
+ \u2022 In astro.config integrations, add:
3225
+ { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
3226
+ injectScript("page-ssr", 'import "${alias}";') } }
3227
+ \u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
3228
+ endpoint route (src/pages/*.ts) that imports a library reading process.env.
3229
+ Injection only applies under a server (on-demand) adapter, not a static build.`
2990
3230
  };
2991
3231
  }
2992
3232
  function viteSpa(_context) {
@@ -3010,6 +3250,7 @@ function seamFor(framework, context) {
3010
3250
  }
3011
3251
 
3012
3252
  // src/commands/init.ts
3253
+ var SCHEMA_SHAPE_BASENAME = "penv.schema";
3013
3254
  var CONFIG_FILE = "penv.config.ts";
3014
3255
  var TSCONFIG_FILE = "tsconfig.json";
3015
3256
  var GITIGNORE_FILE = ".gitignore";
@@ -3020,15 +3261,15 @@ var PACKAGE_FILE = "package.json";
3020
3261
  var DEFAULT_DECISIONS = {
3021
3262
  environments: [],
3022
3263
  inject: false,
3023
- schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
3264
+ schemaFile: import_core16.DEFAULT_SCHEMA_FILE,
3024
3265
  publicPrefixes: [],
3025
3266
  alias: DEFAULT_ALIAS
3026
3267
  };
3027
- var NOT_ENVIRONMENTS = [...import_core15.RESERVED_TOKENS, "example", "sample", "template"];
3268
+ var NOT_ENVIRONMENTS = [...import_core16.RESERVED_TOKENS, "example", "sample", "template"];
3028
3269
  function suggestEnvironments(root) {
3029
3270
  let entries;
3030
3271
  try {
3031
- entries = (0, import_node_fs3.readdirSync)(root);
3272
+ entries = (0, import_node_fs6.readdirSync)(root);
3032
3273
  } catch {
3033
3274
  return [];
3034
3275
  }
@@ -3040,7 +3281,7 @@ function suggestEnvironments(root) {
3040
3281
  const segments = entry.slice(".env.".length).split(".");
3041
3282
  const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
3042
3283
  const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
3043
- 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)) {
3044
3285
  continue;
3045
3286
  }
3046
3287
  found.add(name);
@@ -3048,10 +3289,10 @@ function suggestEnvironments(root) {
3048
3289
  return [...found].sort();
3049
3290
  }
3050
3291
  function emptyFlag(flag) {
3051
- return new import_core15.PenvError(
3292
+ return new import_core16.PenvError(
3052
3293
  "INIT_FLAG_EMPTY",
3053
3294
  `\`--${flag}\` was given without a value`,
3054
- 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."
3055
3296
  );
3056
3297
  }
3057
3298
  function splitEnvironments(value) {
@@ -3072,11 +3313,11 @@ function configOf(decisions) {
3072
3313
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
3073
3314
  }
3074
3315
  function declaredIn(root) {
3075
- const file = (0, import_node_path6.join)(root, CONFIG_FILE);
3076
- 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)) {
3077
3318
  return void 0;
3078
3319
  }
3079
- return (0, import_core15.loadConfigFrom)(file);
3320
+ return (0, import_core16.loadConfigFrom)(file);
3080
3321
  }
3081
3322
  function planInit(root, flags = {}) {
3082
3323
  const declared = declaredIn(root);
@@ -3086,7 +3327,7 @@ function planInit(root, flags = {}) {
3086
3327
  notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
3087
3328
  } else if (detected === void 0) {
3088
3329
  notes.push(
3089
- `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}.`
3090
3331
  );
3091
3332
  } else {
3092
3333
  notes.push(`Detected ${detected.name}.`);
@@ -3096,12 +3337,12 @@ function planInit(root, flags = {}) {
3096
3337
  );
3097
3338
  }
3098
3339
  }
3099
- 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();
3100
3341
  if (flags.schema !== void 0) {
3101
3342
  if (schemaFile.length === 0) {
3102
3343
  throw emptyFlag("schema");
3103
3344
  }
3104
- const error = (0, import_core15.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
3345
+ const error = (0, import_core16.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
3105
3346
  if (error !== void 0) {
3106
3347
  throw error;
3107
3348
  }
@@ -3111,7 +3352,7 @@ function planInit(root, flags = {}) {
3111
3352
  throw emptyFlag("alias");
3112
3353
  }
3113
3354
  if (!ALIAS_NAME.test(alias)) {
3114
- throw new import_core15.PenvError(
3355
+ throw new import_core16.PenvError(
3115
3356
  "INIT_ALIAS_INVALID",
3116
3357
  `\`${alias}\` is not an alias penv can write`,
3117
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.`
@@ -3161,7 +3402,7 @@ function renderPlan(plan2) {
3161
3402
  rows.push([
3162
3403
  ` ${out.dim("schemaFile")}`,
3163
3404
  plan2.decisions.schemaFile,
3164
- 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})`)
3165
3406
  ]);
3166
3407
  for (const prefix of plan2.decisions.publicPrefixes) {
3167
3408
  rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
@@ -3187,17 +3428,18 @@ async function promptForDecisions(plan2, io) {
3187
3428
  );
3188
3429
  io.write("");
3189
3430
  }
3190
- const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3191
3431
  const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
3192
3432
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
3193
3433
  return void 0;
3194
3434
  }
3435
+ const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3195
3436
  return { ...plan2.decisions, environments, inject };
3196
3437
  }
3197
3438
  function seamKindFor(plan2) {
3198
3439
  return seamFor(plan2.detected?.name, {
3199
3440
  alias: plan2.decisions.alias,
3200
- srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
3441
+ srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
3442
+ schemaFile: plan2.decisions.schemaFile
3201
3443
  }).kind;
3202
3444
  }
3203
3445
  async function askInject(io) {
@@ -3210,20 +3452,17 @@ async function askInject(io) {
3210
3452
  }
3211
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.";
3212
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";
3213
- function renderSchemaModule(fields, draft, inject = false) {
3455
+ function renderSchemaShapeModule(fields, draft) {
3214
3456
  const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
3215
- 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";
3216
- const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
3217
3457
  return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
3218
- import { load } from "@penvhq/penv";
3219
3458
 
3220
- // The shape. Import this (or z.infer<typeof schema>) when you only need the
3221
- // 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().
3222
3462
  export const schema = z.object({
3223
3463
  ${body}
3224
3464
  });
3225
3465
 
3226
- ` + loadComment + loadCall + `
3227
3466
  // Registers the schema's shape with penv's types (erased at runtime, so
3228
3467
  // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3229
3468
  // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
@@ -3234,6 +3473,24 @@ declare module "@penvhq/core" {
3234
3473
  }
3235
3474
  `;
3236
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
+ }
3237
3494
  function renderEnvironments(decisions) {
3238
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";
3239
3496
  if (decisions.environments.length === 0) {
@@ -3241,7 +3498,7 @@ function renderEnvironments(decisions) {
3241
3498
  // can read off your codebase, and an environment you do not have is worse
3242
3499
  // than one you have not declared yet. Name yours, and give each a provider:
3243
3500
  // environments: ["development", "production"],
3244
- // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
3501
+ // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
3245
3502
  environments: [],
3246
3503
 
3247
3504
  providers: {},
@@ -3259,7 +3516,7 @@ ${providers} },
3259
3516
  }
3260
3517
  function renderConfigModule(decisions) {
3261
3518
  let body = renderEnvironments(decisions);
3262
- if (decisions.schemaFile !== import_core15.DEFAULT_SCHEMA_FILE) {
3519
+ if (decisions.schemaFile !== import_core16.DEFAULT_SCHEMA_FILE) {
3263
3520
  body += `
3264
3521
  // The module that exports the schema. It is yours \u2014 penv scaffolds it once
3265
3522
  // and never regenerates it \u2014 so this says where you keep it.
@@ -3282,7 +3539,7 @@ ${body}});
3282
3539
  `;
3283
3540
  }
3284
3541
  function renderGitignore(decisions) {
3285
- const inside = (0, import_core15.schemaInsideTree)(configOf(decisions));
3542
+ const inside = (0, import_core16.schemaInsideTree)(configOf(decisions));
3286
3543
  const listed = inside === void 0 ? "" : `${inside}, `;
3287
3544
  return `# Written by penv. Value files hold configuration values and are never
3288
3545
  # committed; only the structure, ${listed}meta, and config are.
@@ -3427,13 +3684,13 @@ ${objectIndent}${source.slice(close)}`;
3427
3684
  ${entryIndent}${member},${source.slice(open + 1)}`;
3428
3685
  }
3429
3686
  function shapeError(what, target, alias) {
3430
- return new import_core15.PenvError(
3687
+ return new import_core16.PenvError(
3431
3688
  "TSCONFIG_SHAPE",
3432
3689
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
3433
3690
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
3434
3691
  );
3435
3692
  }
3436
- 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) {
3437
3694
  const alias = `"${name}": ["${target}"]`;
3438
3695
  const root = skipTrivia(source, 0);
3439
3696
  if (source.charAt(root) !== "{") {
@@ -3497,46 +3754,102 @@ function renderTsconfig(target, alias) {
3497
3754
  `;
3498
3755
  }
3499
3756
  function ensurePenvDir(root) {
3500
- const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
3501
- 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)) {
3502
3759
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
3503
3760
  }
3504
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
3761
+ (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
3505
3762
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
3506
3763
  }
3507
- function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3508
- const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
3509
- 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)) {
3510
3767
  return {
3511
3768
  target: "schema",
3512
3769
  action: "kept",
3513
- text: `Kept ${decisions.schemaFile}`,
3770
+ text: `Kept ${import_core16.SCHEMA_SHAPE_FILE}`,
3514
3771
  note: "(yours \u2014 penv never regenerates it)"
3515
3772
  };
3516
3773
  }
3517
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3518
- (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");
3519
3784
  return {
3520
3785
  target: "schema",
3521
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",
3522
3806
  text: `Generated ${decisions.schemaFile}`,
3523
- 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)"
3524
3808
  };
3525
3809
  }
3526
3810
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
3527
- const file = (0, import_node_path6.join)(root, CONFIG_FILE);
3528
- 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)) {
3529
3813
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
3530
3814
  }
3531
- (0, import_node_fs3.writeFileSync)(file, renderConfigModule(decisions), "utf8");
3815
+ (0, import_node_fs6.writeFileSync)(file, renderConfigModule(decisions), "utf8");
3532
3816
  return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
3533
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
+ }
3534
3847
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3535
3848
  const alias = decisions.alias;
3536
3849
  const imports = alias.startsWith(IMPORTS_PREFIX);
3537
- 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);
3538
3851
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
3539
- if (!(0, import_node_fs3.existsSync)(file)) {
3852
+ if (!(0, import_node_fs6.existsSync)(file)) {
3540
3853
  if (imports) {
3541
3854
  return {
3542
3855
  target: "tsconfig",
@@ -3545,14 +3858,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3545
3858
  note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
3546
3859
  };
3547
3860
  }
3548
- (0, import_node_fs3.writeFileSync)(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
3861
+ (0, import_node_fs6.writeFileSync)(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
3549
3862
  return {
3550
3863
  target: "tsconfig",
3551
3864
  action: "created",
3552
3865
  text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
3553
3866
  };
3554
3867
  }
3555
- const source = (0, import_node_fs3.readFileSync)(file, "utf8");
3868
+ const source = (0, import_node_fs6.readFileSync)(file, "utf8");
3556
3869
  const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
3557
3870
  if (edit.conflict !== void 0) {
3558
3871
  return {
@@ -3569,7 +3882,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3569
3882
  text: `Kept the ${alias} alias in ${where}`
3570
3883
  };
3571
3884
  }
3572
- (0, import_node_fs3.writeFileSync)(file, edit.source, "utf8");
3885
+ (0, import_node_fs6.writeFileSync)(file, edit.source, "utf8");
3573
3886
  return {
3574
3887
  target: "tsconfig",
3575
3888
  action: "updated",
@@ -3577,30 +3890,63 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
3577
3890
  };
3578
3891
  }
3579
3892
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
3580
- 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);
3581
3894
  const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
3582
3895
  const wanted = renderGitignore(decisions);
3583
- 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;
3584
3897
  if (existing === wanted) {
3585
3898
  return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
3586
3899
  }
3587
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
3588
- (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");
3589
3902
  return {
3590
3903
  target: "gitignore",
3591
3904
  action: existing === void 0 ? "created" : "updated",
3592
3905
  text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
3593
3906
  };
3594
3907
  }
3595
- function srcDirOf(root) {
3596
- return (0, import_node_fs3.existsSync)((0, import_node_path6.join)(root, "src")) ? "src/" : "";
3908
+ var INJECT_MIN_VERSION = "0.6.0";
3909
+ function outdatedRuntimeWarning(root) {
3910
+ const version = installedPenvVersion(root);
3911
+ if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
3912
+ return void 0;
3913
+ }
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.`;
3915
+ }
3916
+ function installedPenvVersion(root) {
3917
+ const file = (0, import_node_path8.join)(root, "node_modules", "@penvhq", "penv", "package.json");
3918
+ if (!(0, import_node_fs6.existsSync)(file)) {
3919
+ return void 0;
3920
+ }
3921
+ try {
3922
+ const version = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8")).version;
3923
+ return typeof version === "string" ? version : void 0;
3924
+ } catch {
3925
+ return void 0;
3926
+ }
3597
3927
  }
3598
- function writeSeam(root, decisions = DEFAULT_DECISIONS) {
3928
+ function releaseTriple(version) {
3929
+ const parts = (version.split("-")[0] ?? "").split(".");
3930
+ const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
3931
+ return [at(0), at(1), at(2)];
3932
+ }
3933
+ function isBelow(a, b) {
3934
+ const [am, an, ap] = releaseTriple(a);
3935
+ const [bm, bn, bp] = releaseTriple(b);
3936
+ if (am !== bm) return am < bm;
3937
+ if (an !== bn) return an < bn;
3938
+ return ap < bp;
3939
+ }
3940
+ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3599
3941
  if (!decisions.inject) {
3600
3942
  return void 0;
3601
3943
  }
3602
- const framework = detectFramework(root)?.name;
3603
- const seam = seamFor(framework, { alias: decisions.alias, srcDir: srcDirOf(root) });
3944
+ const seam = seamFor(framework, {
3945
+ alias: decisions.alias,
3946
+ srcDir: srcPrefix(root),
3947
+ schemaFile: decisions.schemaFile
3948
+ });
3949
+ const outdated = outdatedRuntimeWarning(root);
3604
3950
  if (seam.kind === "none") {
3605
3951
  return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
3606
3952
  }
@@ -3609,54 +3955,84 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS) {
3609
3955
  target: "seam",
3610
3956
  action: "info",
3611
3957
  text: "Place the injection seam",
3612
- note: seam.instruction
3958
+ note: withWarning(seam.instruction, outdated)
3613
3959
  };
3614
3960
  }
3615
- const file = (0, import_node_path6.join)(root, ...seam.file.split("/"));
3616
- const notes = seam.notes.length === 0 ? "" : `
3617
- ${seam.notes.map((n) => ` ${n}`).join("\n")}`;
3618
- if ((0, import_node_fs3.existsSync)(file)) {
3961
+ const alsoNote = writeAlso(root, seam.also);
3962
+ const file = (0, import_node_path8.join)(root, ...seam.file.split("/"));
3963
+ const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
3964
+ const notes = baseNotes.length === 0 ? "" : `
3965
+ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
3966
+ if ((0, import_node_fs6.existsSync)(file)) {
3619
3967
  return {
3620
3968
  target: "seam",
3621
3969
  action: "info",
3622
3970
  text: `Add the injection seam to ${seam.file}`,
3623
- note: `${seam.ifPresent}${notes}`
3971
+ note: withWarning(`${seam.ifPresent}${notes}`, outdated)
3624
3972
  };
3625
3973
  }
3626
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3627
- (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");
3628
3976
  return {
3629
3977
  target: "seam",
3630
- action: "created",
3978
+ action: outdated === void 0 ? "created" : "info",
3631
3979
  text: `Wrote ${seam.file} (runs the injection before your app)`,
3632
- ...notes === "" ? {} : { note: notes.trimStart() }
3980
+ ...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
3633
3981
  };
3634
3982
  }
3635
- function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3983
+ function writeAlso(root, also) {
3984
+ if (also === void 0) {
3985
+ return void 0;
3986
+ }
3987
+ const file = (0, import_node_path8.join)(root, ...also.file.split("/"));
3988
+ if ((0, import_node_fs6.existsSync)(file)) {
3989
+ return also.ifPresent;
3990
+ }
3991
+ (0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
3992
+ (0, import_node_fs6.writeFileSync)(file, also.content, "utf8");
3993
+ return `Wrote ${also.file} to register it.`;
3994
+ }
3995
+ function withWarning(note, warning) {
3996
+ if (warning === void 0) {
3997
+ return note;
3998
+ }
3999
+ return note === "" ? warning : `${note}
4000
+ \u26A0 ${warning}`;
4001
+ }
4002
+ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3636
4003
  const steps = [
3637
4004
  ensurePenvDir(root),
3638
- 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),
3639
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),
3640
4014
  writeTsconfigAlias(root, decisions),
3641
4015
  writeGitignore(root, decisions)
3642
4016
  ];
3643
- const seam = writeSeam(root, decisions);
4017
+ const seam = writeSeam(root, decisions, framework);
3644
4018
  return seam === void 0 ? steps : [...steps, seam];
3645
4019
  }
3646
4020
  function runInit(options) {
3647
- const root = (0, import_node_path6.resolve)(options.cwd);
4021
+ const root = (0, import_node_path8.resolve)(options.cwd);
3648
4022
  const decisions = options.decisions ?? planInit(root).decisions;
3649
- return { root, decisions, steps: scaffold(root, [], false, decisions) };
4023
+ return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
3650
4024
  }
3651
4025
  function renderInit(result2) {
3652
4026
  const steps = result2.steps.map((step) => {
3653
4027
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
3654
4028
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
3655
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;
3656
4032
  return [
3657
4033
  ...formatSteps(steps),
3658
4034
  "",
3659
- `${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:`,
3660
4036
  tip(out.cyan("penv set <key>")),
3661
4037
  ...result2.decisions.environments.length === 0 ? [
3662
4038
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
@@ -3684,7 +4060,7 @@ var initCommand = (0, import_citty9.defineCommand)({
3684
4060
  },
3685
4061
  schema: {
3686
4062
  type: "string",
3687
- 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})`
3688
4064
  },
3689
4065
  alias: {
3690
4066
  type: "string",
@@ -3697,7 +4073,7 @@ var initCommand = (0, import_citty9.defineCommand)({
3697
4073
  },
3698
4074
  run({ args }) {
3699
4075
  return guard(async () => {
3700
- const root = (0, import_node_path6.resolve)(process.cwd());
4076
+ const root = (0, import_node_path8.resolve)(process.cwd());
3701
4077
  const environments = environmentsFromFlag(args.env);
3702
4078
  const plan2 = planInit(root, {
3703
4079
  ...args.schema === void 0 ? {} : { schema: args.schema },
@@ -3713,7 +4089,15 @@ var initCommand = (0, import_citty9.defineCommand)({
3713
4089
  if (!asked) {
3714
4090
  write([...plan2.notes, ""]);
3715
4091
  }
3716
- write(renderInit(runInit({ cwd: root, decisions })));
4092
+ write(
4093
+ renderInit(
4094
+ runInit({
4095
+ cwd: root,
4096
+ decisions,
4097
+ ...plan2.detected && { framework: plan2.detected.name }
4098
+ })
4099
+ )
4100
+ );
3717
4101
  });
3718
4102
  }
3719
4103
  });
@@ -3738,7 +4122,7 @@ function inferType(value) {
3738
4122
  }
3739
4123
  function draftFields(entries) {
3740
4124
  return entries.map((entry) => {
3741
- 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(".");
3742
4126
  return {
3743
4127
  key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
3744
4128
  type: inferType(entry.value)
@@ -3749,33 +4133,33 @@ function assertImportable(ref, variable) {
3749
4133
  if (!ref.name.includes(".")) {
3750
4134
  return;
3751
4135
  }
3752
- throw new import_core16.PenvError(
4136
+ throw new import_core17.PenvError(
3753
4137
  "IMPORT_UNPARSEABLE_NAME",
3754
4138
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
3755
4139
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
3756
4140
  );
3757
4141
  }
3758
4142
  function assertNotReserved(ref, variable, where, config) {
3759
- if ((0, import_core16.isReservedToken)(ref.name, config)) {
3760
- 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);
3761
4145
  }
3762
4146
  }
3763
4147
  function assertRoundTrips(ref, variable, config) {
3764
- if ((0, import_core16.roundTripsCleanly)(variable)) {
4148
+ if ((0, import_core17.roundTripsCleanly)(variable)) {
3765
4149
  return;
3766
4150
  }
3767
- if ((0, import_core16.variableName)(ref, config) === variable) {
4151
+ if ((0, import_core17.variableName)(ref, config) === variable) {
3768
4152
  return;
3769
4153
  }
3770
- const generated = (0, import_core16.variableName)(ref, config);
3771
- throw new import_core16.PenvError(
4154
+ const generated = (0, import_core17.variableName)(ref, config);
4155
+ throw new import_core17.PenvError(
3772
4156
  "IMPORT_LOSSY_NAME",
3773
4157
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
3774
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.`
3775
4159
  );
3776
4160
  }
3777
4161
  function collisionsIn(refs, config) {
3778
- const errors = (0, import_core16.checkNameCollisions)(refs, config);
4162
+ const errors = (0, import_core17.checkNameCollisions)(refs, config);
3779
4163
  const first = errors[0];
3780
4164
  if (first !== void 0) {
3781
4165
  throw first;
@@ -3785,10 +4169,10 @@ function assertDeclared(segment, config) {
3785
4169
  if (config.environments.includes(segment)) {
3786
4170
  return segment;
3787
4171
  }
3788
- throw new import_core16.UnknownEnvironmentError(segment, config.environments);
4172
+ throw new import_core17.UnknownEnvironmentError(segment, config.environments);
3789
4173
  }
3790
4174
  function scopeFromFilename(file, config) {
3791
- const name = (0, import_node_path7.basename)(file);
4175
+ const name = (0, import_node_path9.basename)(file);
3792
4176
  const segments = name.split(".");
3793
4177
  const start = segments.indexOf(DOTENV_SEGMENT);
3794
4178
  if (start === -1) {
@@ -3810,13 +4194,13 @@ function scopeFromFilename(file, config) {
3810
4194
  return { kind: "environment-local", environment: assertDeclared(first, config) };
3811
4195
  }
3812
4196
  if (first === LOCAL && rest.length === 2) {
3813
- throw new import_core16.FilenameGrammarError(
4197
+ throw new import_core17.FilenameGrammarError(
3814
4198
  name,
3815
4199
  "`local` precedes the environment segment",
3816
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.`
3817
4201
  );
3818
4202
  }
3819
- throw new import_core16.FilenameGrammarError(
4203
+ throw new import_core17.FilenameGrammarError(
3820
4204
  name,
3821
4205
  `\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
3822
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."
@@ -3831,7 +4215,7 @@ function environmentOf(scope) {
3831
4215
  case "local":
3832
4216
  return void 0;
3833
4217
  default:
3834
- return (0, import_core16.assertNever)(scope, "scope");
4218
+ return (0, import_core17.assertNever)(scope, "scope");
3835
4219
  }
3836
4220
  }
3837
4221
  function scopeWithEnvironment(scope, environment) {
@@ -3844,7 +4228,7 @@ function scopeWithEnvironment(scope, environment) {
3844
4228
  case "environment-local":
3845
4229
  return scope;
3846
4230
  default:
3847
- return (0, import_core16.assertNever)(scope, "scope");
4231
+ return (0, import_core17.assertNever)(scope, "scope");
3848
4232
  }
3849
4233
  }
3850
4234
  function explicitEnvironment(options, source, config) {
@@ -3852,7 +4236,7 @@ function explicitEnvironment(options, source, config) {
3852
4236
  if (value.length > 0) {
3853
4237
  return value;
3854
4238
  }
3855
- throw new import_core16.PenvError(
4239
+ throw new import_core17.PenvError(
3856
4240
  "IMPORT_ENV_FLAG_EMPTY",
3857
4241
  `\`--env\` for the import of ${source} names no environment`,
3858
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.`
@@ -3862,7 +4246,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
3862
4246
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
3863
4247
  return;
3864
4248
  }
3865
- throw new import_core16.PenvError(
4249
+ throw new import_core17.PenvError(
3866
4250
  "IMPORT_ENV_CONFLICT",
3867
4251
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
3868
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.`
@@ -3872,7 +4256,7 @@ function environmentNamed(file, explicit) {
3872
4256
  if (explicit !== void 0 && explicit.trim().length > 0) {
3873
4257
  return explicit.trim();
3874
4258
  }
3875
- const segments = (0, import_node_path7.basename)(file).split(".");
4259
+ const segments = (0, import_node_path9.basename)(file).split(".");
3876
4260
  const start = segments.indexOf(DOTENV_SEGMENT);
3877
4261
  if (start === -1) {
3878
4262
  return void 0;
@@ -3886,15 +4270,15 @@ function decisionsOf(config, cwd) {
3886
4270
  // `import` re-scaffolds env.ts for an existing project; injection is an init
3887
4271
  // choice, so it is not turned on here.
3888
4272
  inject: false,
3889
- schemaFile: (0, import_core16.schemaFileOf)(config),
4273
+ schemaFile: (0, import_core17.schemaFileOf)(config),
3890
4274
  publicPrefixes: config.publicPrefixes ?? [],
3891
4275
  alias: detectAlias(cwd)
3892
4276
  };
3893
4277
  }
3894
4278
  function configInEffect(cwd, environment) {
3895
- const existing = (0, import_core16.findConfigFile)(cwd);
4279
+ const existing = (0, import_core17.findConfigFile)(cwd);
3896
4280
  if (existing !== void 0) {
3897
- const config = (0, import_core16.loadConfigFrom)(existing);
4281
+ const config = (0, import_core17.loadConfigFrom)(existing);
3898
4282
  return { config, decisions: decisionsOf(config, cwd) };
3899
4283
  }
3900
4284
  const planned = planInit(cwd).decisions;
@@ -3906,16 +4290,16 @@ function configInEffect(cwd, environment) {
3906
4290
  return { config: openProject(cwd).config, decisions };
3907
4291
  }
3908
4292
  function importDotenv(options) {
3909
- const cwd = (0, import_node_path7.resolve)(options.cwd);
3910
- const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
3911
- if (!(0, import_node_fs4.existsSync)(file)) {
3912
- 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(
3913
4297
  "IMPORT_FILE_MISSING",
3914
4298
  `There is no file at ${file} to import`,
3915
4299
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
3916
4300
  );
3917
4301
  }
3918
- 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"));
3919
4303
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
3920
4304
  const source = displayPath2(cwd, file);
3921
4305
  const named = scopeFromFilename(file, config);
@@ -3923,10 +4307,10 @@ function importDotenv(options) {
3923
4307
  const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
3924
4308
  assertEnvironmentAgrees(derived, explicit, source);
3925
4309
  const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
3926
- const environment = (0, import_core16.lookupEnvironment)(config, explicit ?? derived);
4310
+ const environment = (0, import_core17.lookupEnvironment)(config, explicit ?? derived);
3927
4311
  const refs = [];
3928
4312
  for (const entry of parsed.entries) {
3929
- const ref = (0, import_core16.refFromVariable)(entry.key);
4313
+ const ref = (0, import_core17.refFromVariable)(entry.key);
3930
4314
  assertImportable(ref, entry.key);
3931
4315
  assertNotReserved(ref, entry.key, source, config);
3932
4316
  assertRoundTrips(ref, entry.key, config);
@@ -3952,7 +4336,8 @@ function importDotenv(options) {
3952
4336
  }
3953
4337
  }
3954
4338
  const backup = `${file}${BACKUP_SUFFIX}`;
3955
- (0, import_node_fs4.copyFileSync)(file, backup);
4339
+ (0, import_node_fs7.copyFileSync)(file, backup);
4340
+ refreshSnapshot(project);
3956
4341
  return {
3957
4342
  root: project.root,
3958
4343
  file,
@@ -3966,7 +4351,7 @@ function importDotenv(options) {
3966
4351
  };
3967
4352
  }
3968
4353
  function displayPath2(root, file) {
3969
- const rel = (0, import_node_path7.relative)(root, file);
4354
+ const rel = (0, import_node_path9.relative)(root, file);
3970
4355
  return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
3971
4356
  }
3972
4357
  function keptSchemaStep(step, variables) {
@@ -4054,7 +4439,7 @@ var importCommand = (0, import_citty10.defineCommand)({
4054
4439
 
4055
4440
  // src/commands/key.ts
4056
4441
  var import_node_crypto = require("crypto");
4057
- var import_core17 = require("@penvhq/core");
4442
+ var import_core18 = require("@penvhq/core");
4058
4443
  var import_citty11 = require("citty");
4059
4444
 
4060
4445
  // src/keychain.ts
@@ -4088,35 +4473,35 @@ function runKeyCreate(options) {
4088
4473
  const environment = targetEnvironment(project, options.environment);
4089
4474
  const declared = project.config.keys?.[environment];
4090
4475
  if (declared === void 0) {
4091
- throw new import_core17.PenvError(
4476
+ throw new import_core18.PenvError(
4092
4477
  "KEY_SOURCE_UNDECLARED",
4093
4478
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
4094
4479
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
4095
4480
  );
4096
4481
  }
4097
- 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");
4098
4483
  if (declared.source === "keychain") {
4099
4484
  const keychain = options.keychain ?? defaultKeychain;
4100
4485
  if (options.force !== true) {
4101
4486
  let existing;
4102
4487
  try {
4103
- existing = keychain.getPassword(import_core17.KEYCHAIN_SERVICE, declared.id);
4488
+ existing = keychain.getPassword(import_core18.KEYCHAIN_SERVICE, declared.id);
4104
4489
  } catch (cause) {
4105
- throw new import_core17.PenvError(
4490
+ throw new import_core18.PenvError(
4106
4491
  "KEYCHAIN_UNAVAILABLE",
4107
4492
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
4108
4493
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
4109
4494
  );
4110
4495
  }
4111
4496
  if (existing !== null) {
4112
- throw new import_core17.PenvError(
4497
+ throw new import_core18.PenvError(
4113
4498
  "KEY_EXISTS",
4114
4499
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
4115
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."
4116
4501
  );
4117
4502
  }
4118
4503
  }
4119
- keychain.setPassword(import_core17.KEYCHAIN_SERVICE, declared.id, key);
4504
+ keychain.setPassword(import_core18.KEYCHAIN_SERVICE, declared.id, key);
4120
4505
  return { source: "keychain", environment, id: declared.id };
4121
4506
  }
4122
4507
  return {
@@ -4184,7 +4569,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
4184
4569
  });
4185
4570
 
4186
4571
  // src/commands/list.ts
4187
- var import_core18 = require("@penvhq/core");
4572
+ var import_core19 = require("@penvhq/core");
4188
4573
  var import_citty12 = require("citty");
4189
4574
  function scopeLabel2(scope) {
4190
4575
  switch (scope.kind) {
@@ -4197,7 +4582,7 @@ function scopeLabel2(scope) {
4197
4582
  case "unscoped":
4198
4583
  return "default";
4199
4584
  default:
4200
- return (0, import_core18.assertNever)(scope, "scope");
4585
+ return (0, import_core19.assertNever)(scope, "scope");
4201
4586
  }
4202
4587
  }
4203
4588
  async function runList(options) {
@@ -4205,12 +4590,12 @@ async function runList(options) {
4205
4590
  const environment = targetEnvironment(project, options.environment);
4206
4591
  const keys = keySourceFor(project, environment);
4207
4592
  const parameters = [];
4208
- 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)) {
4209
4594
  const winner = resolution.winner;
4210
4595
  const scope = winner?.file.scope;
4211
4596
  parameters.push({
4212
4597
  parameter: resolution.parameter,
4213
- variable: (0, import_core18.variableName)(resolution.ref, project.config),
4598
+ variable: (0, import_core19.variableName)(resolution.ref, project.config),
4214
4599
  scope: scope === void 0 ? "absent" : scopeLabel2(scope),
4215
4600
  location: winner?.location,
4216
4601
  encrypted: winner?.file.encrypted === true,
@@ -4263,7 +4648,7 @@ var listCommand = (0, import_citty12.defineCommand)({
4263
4648
  });
4264
4649
 
4265
4650
  // src/commands/mv.ts
4266
- var import_core19 = require("@penvhq/core");
4651
+ var import_core20 = require("@penvhq/core");
4267
4652
  var import_citty13 = require("citty");
4268
4653
  function environmentOf2(file) {
4269
4654
  const scope = file.scope;
@@ -4279,39 +4664,39 @@ async function planFile(project, source, target, parameter) {
4279
4664
  }
4280
4665
  const environment = environmentOf2(source);
4281
4666
  if (environment === void 0) {
4282
- throw new import_core19.PenvError(
4667
+ throw new import_core20.PenvError(
4283
4668
  "SECRET_SCOPE_AMBIGUOUS",
4284
- `${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`,
4285
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."
4286
4671
  );
4287
4672
  }
4288
4673
  const keys = keySourceFor(project, environment);
4289
- const opened = (0, import_core19.openValue)(source, stored, keys);
4674
+ const opened = (0, import_core20.openValue)(source, stored, keys);
4290
4675
  if (opened.kind === "failed") {
4291
- throw new import_core19.PenvError(
4676
+ throw new import_core20.PenvError(
4292
4677
  "VALUE_UNDECRYPTABLE",
4293
- `${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}`,
4294
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."
4295
4680
  );
4296
4681
  }
4297
4682
  return {
4298
4683
  source,
4299
4684
  target,
4300
- contents: (0, import_core19.sealValue)(target, opened.value, keys, parameter, environment),
4685
+ contents: (0, import_core20.sealValue)(target, opened.value, keys, parameter, environment),
4301
4686
  resealed: true
4302
4687
  };
4303
4688
  }
4304
4689
  function filesOf(all, ref) {
4305
- const id = (0, import_core19.parameterId)(ref);
4306
- 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);
4307
4692
  }
4308
4693
  async function runMove(options) {
4309
4694
  const project = openProject(options.cwd);
4310
4695
  const from = refFromKey(options.from, project.config);
4311
4696
  assertWritableKey(options.to);
4312
4697
  const to = refFromKey(options.to, project.config);
4313
- if ((0, import_core19.parameterId)(from) === (0, import_core19.parameterId)(to)) {
4314
- throw new import_core19.PenvError(
4698
+ if ((0, import_core20.parameterId)(from) === (0, import_core20.parameterId)(to)) {
4699
+ throw new import_core20.PenvError(
4315
4700
  "PARAMETER_UNCHANGED",
4316
4701
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
4317
4702
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -4321,24 +4706,24 @@ async function runMove(options) {
4321
4706
  const sources = filesOf(all, from);
4322
4707
  const meta = await project.provider.readMeta(from);
4323
4708
  if (sources.length === 0 && meta === void 0) {
4324
- throw new import_core19.PenvError(
4709
+ throw new import_core20.PenvError(
4325
4710
  "PARAMETER_ABSENT",
4326
- `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`,
4327
4712
  `\`penv list\` shows every parameter penv holds.`
4328
4713
  );
4329
4714
  }
4330
4715
  const occupied2 = filesOf(all, to);
4331
4716
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
4332
- throw new import_core19.PenvError(
4717
+ throw new import_core20.PenvError(
4333
4718
  "PARAMETER_EXISTS",
4334
- `Parameter ${(0, import_core19.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
4335
- `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.`
4336
4721
  );
4337
4722
  }
4338
4723
  const planned = [];
4339
4724
  for (const source of sources) {
4340
4725
  const target = { ...source, namespace: to.namespace, name: to.name };
4341
- 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));
4342
4727
  if (one !== void 0) {
4343
4728
  planned.push(one);
4344
4729
  }
@@ -4355,19 +4740,21 @@ async function runMove(options) {
4355
4740
  if (meta !== void 0) {
4356
4741
  await project.provider.removeMeta(from);
4357
4742
  }
4743
+ refreshSnapshot(project);
4358
4744
  return {
4359
- from: (0, import_core19.parameterId)(from),
4360
- to: (0, import_core19.parameterId)(to),
4745
+ from: (0, import_core20.parameterId)(from),
4746
+ to: (0, import_core20.parameterId)(to),
4361
4747
  files: planned.map((file) => ({
4362
- from: (0, import_core19.formatValueFile)(file.source),
4363
- to: (0, import_core19.formatValueFile)(file.target),
4748
+ from: (0, import_core20.formatValueFile)(file.source),
4749
+ to: (0, import_core20.formatValueFile)(file.target),
4364
4750
  resealed: file.resealed
4365
4751
  })),
4366
4752
  // The meta's path, not the parameter's dotted id: `redis.password` is what
4367
4753
  // the schema calls it and `redis/password.json` is the file, and a report
4368
4754
  // that printed the first while moving the second names no file on disk.
4369
- meta: meta === void 0 ? void 0 : (0, import_core19.formatMetaFile)({ ...to, format: "json" }),
4370
- 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)
4371
4758
  };
4372
4759
  }
4373
4760
  function renderMove(result2) {
@@ -4384,7 +4771,7 @@ function renderMove(result2) {
4384
4771
  lines.push(
4385
4772
  "",
4386
4773
  tip(
4387
- `.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.`
4388
4775
  )
4389
4776
  );
4390
4777
  return lines;
@@ -4411,7 +4798,7 @@ var mvCommand = (0, import_citty13.defineCommand)({
4411
4798
  });
4412
4799
 
4413
4800
  // src/commands/pull.ts
4414
- var import_core20 = require("@penvhq/core");
4801
+ var import_core21 = require("@penvhq/core");
4415
4802
  var import_citty14 = require("citty");
4416
4803
  async function pullProjection(project, source, environment) {
4417
4804
  const tree = localTree(project);
@@ -4424,7 +4811,7 @@ async function pullProjection(project, source, environment) {
4424
4811
  }
4425
4812
  let meta = 0;
4426
4813
  for (const name of [...names].sort()) {
4427
- const ref = (0, import_core20.refFromVariable)(name);
4814
+ const ref = (0, import_core21.refFromVariable)(name);
4428
4815
  if (tree.readMetaSync(ref) === void 0) {
4429
4816
  tree.writeMetaSync(ref, {});
4430
4817
  meta += 1;
@@ -4447,7 +4834,7 @@ async function runPull(options) {
4447
4834
  if (source.type === LOCAL_TREE_TYPE) {
4448
4835
  return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
4449
4836
  }
4450
- if ((0, import_core20.holdsProjection)(source)) {
4837
+ if ((0, import_core21.holdsProjection)(source)) {
4451
4838
  return pullProjection(project, source, environment);
4452
4839
  }
4453
4840
  const tree = localTree(project);
@@ -4471,6 +4858,7 @@ async function runPull(options) {
4471
4858
  tree.writeMetaSync(ref, block);
4472
4859
  meta += 1;
4473
4860
  }
4861
+ refreshSnapshot(project);
4474
4862
  return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
4475
4863
  }
4476
4864
  function renderPull(result2) {
@@ -4539,7 +4927,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
4539
4927
  });
4540
4928
 
4541
4929
  // src/commands/remove.ts
4542
- var import_core21 = require("@penvhq/core");
4930
+ var import_core22 = require("@penvhq/core");
4543
4931
  var import_citty15 = require("citty");
4544
4932
  async function runRemove(options) {
4545
4933
  const project = openProject(options.cwd);
@@ -4557,12 +4945,13 @@ async function runRemove(options) {
4557
4945
  continue;
4558
4946
  }
4559
4947
  await project.provider.remove(file);
4560
- removed.push((0, import_core21.formatValueFile)(file));
4948
+ removed.push((0, import_core22.formatValueFile)(file));
4561
4949
  }
4950
+ refreshSnapshot(project);
4562
4951
  return {
4563
4952
  parameter: options.key,
4564
4953
  removed,
4565
- considered: files.map((file) => (0, import_core21.formatValueFile)(file))
4954
+ considered: files.map((file) => (0, import_core22.formatValueFile)(file))
4566
4955
  };
4567
4956
  }
4568
4957
  function renderRemove(result2) {
@@ -4611,7 +5000,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
4611
5000
  });
4612
5001
 
4613
5002
  // src/commands/rotate.ts
4614
- var import_core22 = require("@penvhq/core");
5003
+ var import_core23 = require("@penvhq/core");
4615
5004
  var import_citty16 = require("citty");
4616
5005
  function rotatingFile(ref, environment) {
4617
5006
  return {
@@ -4631,13 +5020,14 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
4631
5020
  value,
4632
5021
  environment
4633
5022
  });
5023
+ refreshSnapshot(project);
4634
5024
  return;
4635
5025
  }
4636
5026
  await provider.write(rotatingFile(ref, environment), value);
4637
5027
  }
4638
5028
  function requireNewValue(value, phase, key) {
4639
5029
  if (value === void 0) {
4640
- throw new import_core22.PenvError(
5030
+ throw new import_core23.PenvError(
4641
5031
  "ROTATION_NO_VALUE",
4642
5032
  `A ${phase} rotation of ${key} writes a new value, and none was given`,
4643
5033
  "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
@@ -4646,8 +5036,8 @@ function requireNewValue(value, phase, key) {
4646
5036
  return value;
4647
5037
  }
4648
5038
  function requireRetaining(provider, environment) {
4649
- if (!(0, import_core22.retainsPrevious)(provider)) {
4650
- throw new import_core22.PenvError(
5039
+ if (!(0, import_core23.retainsPrevious)(provider)) {
5040
+ throw new import_core23.PenvError(
4651
5041
  "ROTATION_NOT_RETAINING",
4652
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`,
4653
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."
@@ -4660,8 +5050,8 @@ async function runRotate(options) {
4660
5050
  const environment = targetEnvironment(project, options.environment);
4661
5051
  const ref = refFromKey(options.key, project.config);
4662
5052
  const source = await sourceProviderFor(project, environment);
4663
- if (!(0, import_core22.holdsRecords)(source)) {
4664
- throw new import_core22.PenvError(
5053
+ if (!(0, import_core23.holdsRecords)(source)) {
5054
+ throw new import_core23.PenvError(
4665
5055
  "ROTATION_NOT_RECORDS",
4666
5056
  `Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
4667
5057
  "Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
@@ -4670,9 +5060,9 @@ async function runRotate(options) {
4670
5060
  const provider = source;
4671
5061
  const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
4672
5062
  const before = await provider.readMeta(ref);
4673
- const { mechanism } = (0, import_core22.rotationOf)(before, environment);
5063
+ const { mechanism } = (0, import_core23.rotationOf)(before, environment);
4674
5064
  if (mechanism === void 0) {
4675
- throw new import_core22.PenvError(
5065
+ throw new import_core23.PenvError(
4676
5066
  "ROTATION_NO_MECHANISM",
4677
5067
  `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
4678
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.'
@@ -4682,7 +5072,7 @@ async function runRotate(options) {
4682
5072
  const complete = options.complete === true;
4683
5073
  if (mechanism === "atomic-cutover") {
4684
5074
  if (begin || complete) {
4685
- throw new import_core22.PenvError(
5075
+ throw new import_core23.PenvError(
4686
5076
  "ROTATION_MECHANISM_MISMATCH",
4687
5077
  `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
4688
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."
@@ -4690,13 +5080,13 @@ async function runRotate(options) {
4690
5080
  }
4691
5081
  const value = requireNewValue(options.value, "cutover", options.key);
4692
5082
  await writeRotatedValue(project, provider, ref, environment, value);
4693
- const after2 = (0, import_core22.completeRotation)(before, environment, nowIso);
5083
+ const after2 = (0, import_core23.completeRotation)(before, environment, nowIso);
4694
5084
  await provider.writeMeta(ref, after2);
4695
5085
  return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
4696
5086
  }
4697
5087
  const retaining = requireRetaining(provider, environment);
4698
5088
  if (begin === complete) {
4699
- throw new import_core22.PenvError(
5089
+ throw new import_core23.PenvError(
4700
5090
  "ROTATION_PHASE_REQUIRED",
4701
5091
  `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
4702
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."
@@ -4705,16 +5095,16 @@ async function runRotate(options) {
4705
5095
  if (begin) {
4706
5096
  const value = requireNewValue(options.value, "begin", options.key);
4707
5097
  await writeRotatedValue(project, retaining, ref, environment, value);
4708
- const after2 = (0, import_core22.beginRotation)(before, environment, nowIso);
5098
+ const after2 = (0, import_core23.beginRotation)(before, environment, nowIso);
4709
5099
  await retaining.writeMeta(ref, after2);
4710
5100
  return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
4711
5101
  }
4712
- const after = (0, import_core22.completeRotation)(before, environment, nowIso);
5102
+ const after = (0, import_core23.completeRotation)(before, environment, nowIso);
4713
5103
  await retaining.writeMeta(ref, after);
4714
5104
  return result(ref, environment, mechanism, "complete", retaining.type, false, after);
4715
5105
  }
4716
5106
  function result(ref, environment, mechanism, phase, source, wroteValue, after) {
4717
- const { state, rotatingSince, lastRotated } = (0, import_core22.rotationOf)(after, environment);
5107
+ const { state, rotatingSince, lastRotated } = (0, import_core23.rotationOf)(after, environment);
4718
5108
  return {
4719
5109
  parameter: [...ref.namespace, ref.name].join("/"),
4720
5110
  environment,
@@ -4798,15 +5188,57 @@ var rotateCommand = (0, import_citty16.defineCommand)({
4798
5188
  }
4799
5189
  });
4800
5190
 
4801
- // src/commands/watch.ts
4802
- var import_node_fs5 = require("fs");
4803
- var import_node_path8 = require("path");
4804
- var import_core23 = require("@penvhq/core");
5191
+ // src/commands/snapshot.ts
4805
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");
4806
5238
  var DEBOUNCE_MS = 100;
4807
5239
  function runWatch(options) {
4808
5240
  const project = openProject(options.cwd);
4809
- const configFile = (0, import_node_path8.basename)(project.configFile);
5241
+ const configFile = (0, import_node_path10.basename)(project.configFile);
4810
5242
  const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
4811
5243
  const watchers = /* @__PURE__ */ new Set();
4812
5244
  let timer;
@@ -4859,22 +5291,22 @@ function runWatch(options) {
4859
5291
  watcher.close();
4860
5292
  }
4861
5293
  function armRecovery(target, recursive, only) {
4862
- const parent = (0, import_node_path8.dirname)(target);
4863
- 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);
4864
5296
  let recovery;
4865
5297
  try {
4866
- recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
5298
+ recovery = (0, import_node_fs8.watch)(parent, { recursive: false }, (_event, filename) => {
4867
5299
  if (closed || recovery === void 0) {
4868
5300
  return;
4869
5301
  }
4870
- if (!(0, import_node_fs5.existsSync)(parent)) {
5302
+ if (!(0, import_node_fs8.existsSync)(parent)) {
4871
5303
  stop(recovery);
4872
5304
  return;
4873
5305
  }
4874
- if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
5306
+ if (filename !== null && (0, import_node_path10.basename)(filename) !== name) {
4875
5307
  return;
4876
5308
  }
4877
- if (!(0, import_node_fs5.existsSync)(target)) {
5309
+ if (!(0, import_node_fs8.existsSync)(target)) {
4878
5310
  return;
4879
5311
  }
4880
5312
  stop(recovery);
@@ -4894,11 +5326,11 @@ function runWatch(options) {
4894
5326
  }
4895
5327
  function addWatcher(target, recursive, only) {
4896
5328
  let watcher;
4897
- 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) => {
4898
5330
  if (closed) {
4899
5331
  return;
4900
5332
  }
4901
- if (!(0, import_node_fs5.existsSync)(target)) {
5333
+ if (!(0, import_node_fs8.existsSync)(target)) {
4902
5334
  if (watcher !== void 0) {
4903
5335
  stop(watcher);
4904
5336
  }
@@ -4906,7 +5338,7 @@ function runWatch(options) {
4906
5338
  schedule();
4907
5339
  return;
4908
5340
  }
4909
- 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)) {
4910
5342
  return;
4911
5343
  }
4912
5344
  schedule();
@@ -4933,10 +5365,11 @@ function runWatch(options) {
4933
5365
  watchers.add(watcher);
4934
5366
  }
4935
5367
  addWatcher(project.penvDir, true);
4936
- addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
4937
- if ((0, import_core23.schemaInsideTree)(project.config) === void 0) {
4938
- const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core23.schemaFileOf)(project.config));
4939
- 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));
4940
5373
  }
4941
5374
  void validate();
4942
5375
  return {
@@ -4979,7 +5412,7 @@ function renderDrift(drift, environment) {
4979
5412
  }
4980
5413
  return lines;
4981
5414
  }
4982
- var watchCommand = (0, import_citty17.defineCommand)({
5415
+ var watchCommand = (0, import_citty18.defineCommand)({
4983
5416
  meta: {
4984
5417
  name: "watch",
4985
5418
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -5018,7 +5451,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
5018
5451
  });
5019
5452
 
5020
5453
  // src/index.ts
5021
- var main = (0, import_citty18.defineCommand)({
5454
+ var main = (0, import_citty19.defineCommand)({
5022
5455
  meta: {
5023
5456
  name: "penv",
5024
5457
  description: "Configuration that shares a data model with your production secret manager"
@@ -5039,14 +5472,15 @@ var main = (0, import_citty18.defineCommand)({
5039
5472
  encrypt: encryptCommand,
5040
5473
  decrypt: decryptCommand,
5041
5474
  key: keyCommand,
5475
+ snapshot: snapshotCommand,
5042
5476
  validate: validateCommand,
5043
5477
  doctor: doctorCommand,
5044
5478
  watch: watchCommand
5045
5479
  }
5046
5480
  });
5047
5481
  function runMain() {
5048
- (0, import_core24.setKeychain)(defaultKeychain);
5049
- return (0, import_citty18.runMain)(main);
5482
+ (0, import_core25.setKeychain)(defaultKeychain);
5483
+ return (0, import_citty19.runMain)(main);
5050
5484
  }
5051
5485
  // Annotate the CommonJS export names for ESM import in node:
5052
5486
  0 && (module.exports = {
@@ -5061,6 +5495,7 @@ function runMain() {
5061
5495
  renderPull,
5062
5496
  renderPush,
5063
5497
  renderRotate,
5498
+ renderSnapshot,
5064
5499
  renderWatch,
5065
5500
  runDecrypt,
5066
5501
  runDoctor,
@@ -5078,6 +5513,7 @@ function runMain() {
5078
5513
  runRemove,
5079
5514
  runRotate,
5080
5515
  runSet,
5516
+ runSnapshot,
5081
5517
  runValidate,
5082
5518
  runWatch
5083
5519
  });