@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 +758 -322
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +573 -136
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
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
|
|
56
|
-
var
|
|
57
|
+
var import_core25 = require("@penvhq/core");
|
|
58
|
+
var import_citty19 = require("citty");
|
|
57
59
|
|
|
58
60
|
// src/commands/doctor.ts
|
|
59
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
1225
|
+
var import_node_path4 = require("path");
|
|
1082
1226
|
var import_node_url2 = require("url");
|
|
1083
|
-
var
|
|
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
|
|
1245
|
+
if (error instanceof import_core8.ReservedTokenError) {
|
|
1102
1246
|
return { kind: "reserved", subject: error.token, ...base };
|
|
1103
1247
|
}
|
|
1104
|
-
if (error instanceof
|
|
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
|
-
|
|
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,
|
|
1159
|
-
const
|
|
1160
|
-
|
|
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,
|
|
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[
|
|
1313
|
+
const previousHarvest = process.env[import_core8.SCHEMA_HARVEST_ENV];
|
|
1166
1314
|
process.env.PENV_ENV = environment;
|
|
1167
|
-
process.env[
|
|
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[
|
|
1342
|
+
delete process.env[import_core8.SCHEMA_HARVEST_ENV];
|
|
1195
1343
|
} else {
|
|
1196
|
-
process.env[
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1413
|
+
place(object, (0, import_core8.accessPath)(resolution.ref), resolution.value);
|
|
1265
1414
|
}
|
|
1266
1415
|
}
|
|
1267
|
-
drift = computeDrift({
|
|
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 ${
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1606
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2099
|
+
if (!(0, import_core9.isStuck)(meta, environment, now, stuckThresholdMs)) {
|
|
1941
2100
|
continue;
|
|
1942
2101
|
}
|
|
1943
|
-
const { rotatingSince } = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2381
|
+
var import_core11 = require("@penvhq/core");
|
|
2161
2382
|
var import_citty5 = require("citty");
|
|
2162
2383
|
|
|
2163
2384
|
// src/commands/set.ts
|
|
2164
|
-
var
|
|
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
|
|
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
|
|
2418
|
+
throw new import_core10.PenvError(
|
|
2198
2419
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
2199
|
-
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
2540
|
+
throw new import_core11.PenvError(
|
|
2319
2541
|
"PARAMETER_ABSENT",
|
|
2320
|
-
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0,
|
|
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,
|
|
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,
|
|
2330
|
-
removed: (0,
|
|
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,
|
|
2338
|
-
if ((0,
|
|
2339
|
-
throw new
|
|
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
|
|
2570
|
+
throw new import_core11.PenvError(
|
|
2348
2571
|
"PARAMETER_ABSENT",
|
|
2349
|
-
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0,
|
|
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,
|
|
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,
|
|
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,
|
|
2367
|
-
removed: (0,
|
|
2590
|
+
location: (0, import_core11.formatValueFile)(plain),
|
|
2591
|
+
removed: (0, import_core11.formatValueFile)(sealed)
|
|
2368
2592
|
};
|
|
2369
2593
|
}
|
|
2370
|
-
var UndecryptableAt = class extends
|
|
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
|
|
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
|
|
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
|
|
2576
|
-
var
|
|
2577
|
-
var
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
2833
|
+
const description = (0, import_core13.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
|
|
2610
2834
|
entries.push({
|
|
2611
|
-
key: (0,
|
|
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,
|
|
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,
|
|
2629
|
-
(0,
|
|
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,
|
|
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
|
|
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,
|
|
2689
|
-
const value = (0,
|
|
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
|
|
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,
|
|
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
|
|
2768
|
-
var
|
|
2769
|
-
var
|
|
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
|
|
2774
|
-
var
|
|
2775
|
-
var
|
|
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,
|
|
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,
|
|
2803
|
-
return (0,
|
|
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 = (
|
|
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:
|
|
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,
|
|
2835
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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
|
|
2883
|
-
var
|
|
3109
|
+
var import_node_fs6 = require("fs");
|
|
3110
|
+
var import_node_path8 = require("path");
|
|
2884
3111
|
var import_promises = require("readline/promises");
|
|
2885
|
-
var
|
|
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
|
|
2955
|
-
//
|
|
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
|
-
|
|
2961
|
-
|
|
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,
|
|
2966
|
-
const
|
|
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(
|
|
3219
|
+
function astro({ alias }) {
|
|
2987
3220
|
return {
|
|
2988
3221
|
kind: "instruct",
|
|
2989
|
-
instruction:
|
|
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:
|
|
3264
|
+
schemaFile: import_core16.DEFAULT_SCHEMA_FILE,
|
|
3024
3265
|
publicPrefixes: [],
|
|
3025
3266
|
alias: DEFAULT_ALIAS
|
|
3026
3267
|
};
|
|
3027
|
-
var NOT_ENVIRONMENTS = [...
|
|
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,
|
|
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,
|
|
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
|
|
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 ${
|
|
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,
|
|
3076
|
-
if (!(0,
|
|
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,
|
|
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 ${
|
|
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,
|
|
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,
|
|
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
|
|
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 ===
|
|
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
|
|
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
|
|
3221
|
-
// type \u2014 tests, tooling \u2014 so you
|
|
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-
|
|
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 !==
|
|
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,
|
|
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
|
|
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 =
|
|
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,
|
|
3501
|
-
if ((0,
|
|
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,
|
|
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
|
|
3508
|
-
const file = (0,
|
|
3509
|
-
if ((0,
|
|
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 ${
|
|
3770
|
+
text: `Kept ${import_core16.SCHEMA_SHAPE_FILE}`,
|
|
3514
3771
|
note: "(yours \u2014 penv never regenerates it)"
|
|
3515
3772
|
};
|
|
3516
3773
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
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:
|
|
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,
|
|
3528
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3588
|
-
(0,
|
|
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
|
-
|
|
3596
|
-
|
|
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
|
|
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
|
|
3603
|
-
|
|
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
|
|
3616
|
-
const
|
|
3617
|
-
|
|
3618
|
-
|
|
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,
|
|
3627
|
-
(0,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 ${
|
|
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: ${
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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,
|
|
3760
|
-
throw new
|
|
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,
|
|
4148
|
+
if ((0, import_core17.roundTripsCleanly)(variable)) {
|
|
3765
4149
|
return;
|
|
3766
4150
|
}
|
|
3767
|
-
if ((0,
|
|
4151
|
+
if ((0, import_core17.variableName)(ref, config) === variable) {
|
|
3768
4152
|
return;
|
|
3769
4153
|
}
|
|
3770
|
-
const generated = (0,
|
|
3771
|
-
throw new
|
|
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,
|
|
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
|
|
4172
|
+
throw new import_core17.UnknownEnvironmentError(segment, config.environments);
|
|
3789
4173
|
}
|
|
3790
4174
|
function scopeFromFilename(file, config) {
|
|
3791
|
-
const name = (0,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
4279
|
+
const existing = (0, import_core17.findConfigFile)(cwd);
|
|
3896
4280
|
if (existing !== void 0) {
|
|
3897
|
-
const config = (0,
|
|
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,
|
|
3910
|
-
const file = (0,
|
|
3911
|
-
if (!(0,
|
|
3912
|
-
throw new
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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)(
|
|
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(
|
|
4488
|
+
existing = keychain.getPassword(import_core18.KEYCHAIN_SERVICE, declared.id);
|
|
4104
4489
|
} catch (cause) {
|
|
4105
|
-
throw new
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
4667
|
+
throw new import_core20.PenvError(
|
|
4283
4668
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
4284
|
-
`${PENV_DIR}/${(0,
|
|
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,
|
|
4674
|
+
const opened = (0, import_core20.openValue)(source, stored, keys);
|
|
4290
4675
|
if (opened.kind === "failed") {
|
|
4291
|
-
throw new
|
|
4676
|
+
throw new import_core20.PenvError(
|
|
4292
4677
|
"VALUE_UNDECRYPTABLE",
|
|
4293
|
-
`${PENV_DIR}/${(0,
|
|
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,
|
|
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,
|
|
4306
|
-
return all.filter((file) => (0,
|
|
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,
|
|
4314
|
-
throw new
|
|
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
|
|
4709
|
+
throw new import_core20.PenvError(
|
|
4325
4710
|
"PARAMETER_ABSENT",
|
|
4326
|
-
`Parameter ${(0,
|
|
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
|
|
4717
|
+
throw new import_core20.PenvError(
|
|
4333
4718
|
"PARAMETER_EXISTS",
|
|
4334
|
-
`Parameter ${(0,
|
|
4335
|
-
`Remove or rename ${(0,
|
|
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,
|
|
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,
|
|
4360
|
-
to: (0,
|
|
4745
|
+
from: (0, import_core20.parameterId)(from),
|
|
4746
|
+
to: (0, import_core20.parameterId)(to),
|
|
4361
4747
|
files: planned.map((file) => ({
|
|
4362
|
-
from: (0,
|
|
4363
|
-
to: (0,
|
|
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,
|
|
4370
|
-
schema: { was: (0,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
4650
|
-
throw new
|
|
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,
|
|
4664
|
-
throw new
|
|
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,
|
|
5063
|
+
const { mechanism } = (0, import_core23.rotationOf)(before, environment);
|
|
4674
5064
|
if (mechanism === void 0) {
|
|
4675
|
-
throw new
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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/
|
|
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,
|
|
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,
|
|
4863
|
-
const name = (0,
|
|
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,
|
|
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,
|
|
5302
|
+
if (!(0, import_node_fs8.existsSync)(parent)) {
|
|
4871
5303
|
stop(recovery);
|
|
4872
5304
|
return;
|
|
4873
5305
|
}
|
|
4874
|
-
if (filename !== null && (0,
|
|
5306
|
+
if (filename !== null && (0, import_node_path10.basename)(filename) !== name) {
|
|
4875
5307
|
return;
|
|
4876
5308
|
}
|
|
4877
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
5049
|
-
return (0,
|
|
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
|
});
|