@penvhq/cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +659 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -7
- package/dist/index.d.ts +49 -7
- package/dist/index.js +473 -115
- 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,8 +3023,8 @@ 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
3030
|
const dir = srcPrefix(cwd);
|
|
@@ -2812,10 +3036,10 @@ function schemaFileFor(cwd) {
|
|
|
2812
3036
|
if (!occupied(cwd, beside)) {
|
|
2813
3037
|
return { file: beside, displaced: preferred };
|
|
2814
3038
|
}
|
|
2815
|
-
return { file:
|
|
3039
|
+
return { file: import_core15.DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2816
3040
|
}
|
|
2817
3041
|
function srcPrefix(cwd) {
|
|
2818
|
-
return (0,
|
|
3042
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path7.join)(cwd, "src")) ? "src/" : "";
|
|
2819
3043
|
}
|
|
2820
3044
|
function dependenciesOf(cwd) {
|
|
2821
3045
|
const manifest = manifestOf(cwd);
|
|
@@ -2834,13 +3058,13 @@ function dependenciesOf(cwd) {
|
|
|
2834
3058
|
return names;
|
|
2835
3059
|
}
|
|
2836
3060
|
function manifestOf(cwd) {
|
|
2837
|
-
const file = (0,
|
|
2838
|
-
if (!(0,
|
|
3061
|
+
const file = (0, import_node_path7.join)(cwd, "package.json");
|
|
3062
|
+
if (!(0, import_node_fs5.existsSync)(file)) {
|
|
2839
3063
|
return void 0;
|
|
2840
3064
|
}
|
|
2841
3065
|
let manifest;
|
|
2842
3066
|
try {
|
|
2843
|
-
manifest = JSON.parse((0,
|
|
3067
|
+
manifest = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
|
|
2844
3068
|
} catch {
|
|
2845
3069
|
return void 0;
|
|
2846
3070
|
}
|
|
@@ -2856,7 +3080,7 @@ function detectFramework(cwd) {
|
|
|
2856
3080
|
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2857
3081
|
}
|
|
2858
3082
|
}
|
|
2859
|
-
if ((0,
|
|
3083
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path7.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2860
3084
|
return detectedFrom(cwd, "Bun", []);
|
|
2861
3085
|
}
|
|
2862
3086
|
return void 0;
|
|
@@ -2882,10 +3106,10 @@ function hasImportsBlock(cwd) {
|
|
|
2882
3106
|
}
|
|
2883
3107
|
|
|
2884
3108
|
// src/commands/init.ts
|
|
2885
|
-
var
|
|
2886
|
-
var
|
|
3109
|
+
var import_node_fs6 = require("fs");
|
|
3110
|
+
var import_node_path8 = require("path");
|
|
2887
3111
|
var import_promises = require("readline/promises");
|
|
2888
|
-
var
|
|
3112
|
+
var import_core16 = require("@penvhq/core");
|
|
2889
3113
|
var import_citty9 = require("citty");
|
|
2890
3114
|
|
|
2891
3115
|
// src/seams.ts
|
|
@@ -3026,6 +3250,7 @@ function seamFor(framework, context) {
|
|
|
3026
3250
|
}
|
|
3027
3251
|
|
|
3028
3252
|
// src/commands/init.ts
|
|
3253
|
+
var SCHEMA_SHAPE_BASENAME = "penv.schema";
|
|
3029
3254
|
var CONFIG_FILE = "penv.config.ts";
|
|
3030
3255
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
3031
3256
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -3036,15 +3261,15 @@ var PACKAGE_FILE = "package.json";
|
|
|
3036
3261
|
var DEFAULT_DECISIONS = {
|
|
3037
3262
|
environments: [],
|
|
3038
3263
|
inject: false,
|
|
3039
|
-
schemaFile:
|
|
3264
|
+
schemaFile: import_core16.DEFAULT_SCHEMA_FILE,
|
|
3040
3265
|
publicPrefixes: [],
|
|
3041
3266
|
alias: DEFAULT_ALIAS
|
|
3042
3267
|
};
|
|
3043
|
-
var NOT_ENVIRONMENTS = [...
|
|
3268
|
+
var NOT_ENVIRONMENTS = [...import_core16.RESERVED_TOKENS, "example", "sample", "template"];
|
|
3044
3269
|
function suggestEnvironments(root) {
|
|
3045
3270
|
let entries;
|
|
3046
3271
|
try {
|
|
3047
|
-
entries = (0,
|
|
3272
|
+
entries = (0, import_node_fs6.readdirSync)(root);
|
|
3048
3273
|
} catch {
|
|
3049
3274
|
return [];
|
|
3050
3275
|
}
|
|
@@ -3056,7 +3281,7 @@ function suggestEnvironments(root) {
|
|
|
3056
3281
|
const segments = entry.slice(".env.".length).split(".");
|
|
3057
3282
|
const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
|
|
3058
3283
|
const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
|
|
3059
|
-
if (name === void 0 || !(0,
|
|
3284
|
+
if (name === void 0 || !(0, import_core16.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
|
|
3060
3285
|
continue;
|
|
3061
3286
|
}
|
|
3062
3287
|
found.add(name);
|
|
@@ -3064,10 +3289,10 @@ function suggestEnvironments(root) {
|
|
|
3064
3289
|
return [...found].sort();
|
|
3065
3290
|
}
|
|
3066
3291
|
function emptyFlag(flag) {
|
|
3067
|
-
return new
|
|
3292
|
+
return new import_core16.PenvError(
|
|
3068
3293
|
"INIT_FLAG_EMPTY",
|
|
3069
3294
|
`\`--${flag}\` was given without a value`,
|
|
3070
|
-
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${
|
|
3295
|
+
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core16.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
|
|
3071
3296
|
);
|
|
3072
3297
|
}
|
|
3073
3298
|
function splitEnvironments(value) {
|
|
@@ -3088,11 +3313,11 @@ function configOf(decisions) {
|
|
|
3088
3313
|
return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
|
|
3089
3314
|
}
|
|
3090
3315
|
function declaredIn(root) {
|
|
3091
|
-
const file = (0,
|
|
3092
|
-
if (!(0,
|
|
3316
|
+
const file = (0, import_node_path8.join)(root, CONFIG_FILE);
|
|
3317
|
+
if (!(0, import_node_fs6.existsSync)(file)) {
|
|
3093
3318
|
return void 0;
|
|
3094
3319
|
}
|
|
3095
|
-
return (0,
|
|
3320
|
+
return (0, import_core16.loadConfigFrom)(file);
|
|
3096
3321
|
}
|
|
3097
3322
|
function planInit(root, flags = {}) {
|
|
3098
3323
|
const declared = declaredIn(root);
|
|
@@ -3102,7 +3327,7 @@ function planInit(root, flags = {}) {
|
|
|
3102
3327
|
notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
|
|
3103
3328
|
} else if (detected === void 0) {
|
|
3104
3329
|
notes.push(
|
|
3105
|
-
`No framework detected in package.json \u2014 the schema goes to ${
|
|
3330
|
+
`No framework detected in package.json \u2014 the schema goes to ${import_core16.DEFAULT_SCHEMA_FILE}.`
|
|
3106
3331
|
);
|
|
3107
3332
|
} else {
|
|
3108
3333
|
notes.push(`Detected ${detected.name}.`);
|
|
@@ -3112,12 +3337,12 @@ function planInit(root, flags = {}) {
|
|
|
3112
3337
|
);
|
|
3113
3338
|
}
|
|
3114
3339
|
}
|
|
3115
|
-
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0,
|
|
3340
|
+
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core16.schemaFileOf)(declared) : detected?.schemaFile ?? import_core16.DEFAULT_SCHEMA_FILE : flags.schema.trim();
|
|
3116
3341
|
if (flags.schema !== void 0) {
|
|
3117
3342
|
if (schemaFile.length === 0) {
|
|
3118
3343
|
throw emptyFlag("schema");
|
|
3119
3344
|
}
|
|
3120
|
-
const error = (0,
|
|
3345
|
+
const error = (0, import_core16.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
|
|
3121
3346
|
if (error !== void 0) {
|
|
3122
3347
|
throw error;
|
|
3123
3348
|
}
|
|
@@ -3127,7 +3352,7 @@ function planInit(root, flags = {}) {
|
|
|
3127
3352
|
throw emptyFlag("alias");
|
|
3128
3353
|
}
|
|
3129
3354
|
if (!ALIAS_NAME.test(alias)) {
|
|
3130
|
-
throw new
|
|
3355
|
+
throw new import_core16.PenvError(
|
|
3131
3356
|
"INIT_ALIAS_INVALID",
|
|
3132
3357
|
`\`${alias}\` is not an alias penv can write`,
|
|
3133
3358
|
`An alias is \`@name\` \u2014 a tsconfig \`paths\` entry a bundler resolves \u2014 or \`#name\`, a package.json \`imports\` entry Node resolves itself. Those are the two things a module specifier can be that is not a package.`
|
|
@@ -3177,7 +3402,7 @@ function renderPlan(plan2) {
|
|
|
3177
3402
|
rows.push([
|
|
3178
3403
|
` ${out.dim("schemaFile")}`,
|
|
3179
3404
|
plan2.decisions.schemaFile,
|
|
3180
|
-
plan2.decisions.schemaFile ===
|
|
3405
|
+
plan2.decisions.schemaFile === import_core16.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core16.DEFAULT_SCHEMA_FILE})`)
|
|
3181
3406
|
]);
|
|
3182
3407
|
for (const prefix of plan2.decisions.publicPrefixes) {
|
|
3183
3408
|
rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
|
|
@@ -3227,20 +3452,17 @@ async function askInject(io) {
|
|
|
3227
3452
|
}
|
|
3228
3453
|
var EMPTY_SCHEMA_BODY = " // One key per parameter, e.g. `databaseUrl: z.url(),`. Nesting a key nests\n // the parameter: `redis: z.object({ password: z.string() })` is redis/password.";
|
|
3229
3454
|
var DRAFT_HEADER = "// DRAFT \u2014 generated by `penv import` from one sample of each value, and yours\n// to correct. Single-sample inference cannot know that a boolean seen as `true`\n// must also accept `1`/`0`, or that a string is really a URL. penv scaffolds\n// this file once and never regenerates it, so edits here are safe.\n";
|
|
3230
|
-
function
|
|
3455
|
+
function renderSchemaShapeModule(fields, draft) {
|
|
3231
3456
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3232
|
-
const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly (WorkOS, Prisma) instead of importing @env.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid.\n";
|
|
3233
|
-
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3234
3457
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3235
|
-
import { load } from "@penvhq/penv";
|
|
3236
3458
|
|
|
3237
|
-
// The shape
|
|
3238
|
-
// 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().
|
|
3239
3462
|
export const schema = z.object({
|
|
3240
3463
|
${body}
|
|
3241
3464
|
});
|
|
3242
3465
|
|
|
3243
|
-
` + loadComment + loadCall + `
|
|
3244
3466
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3245
3467
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3246
3468
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3251,6 +3473,24 @@ declare module "@penvhq/core" {
|
|
|
3251
3473
|
}
|
|
3252
3474
|
`;
|
|
3253
3475
|
}
|
|
3476
|
+
function shapeSpecifier(schemaFile) {
|
|
3477
|
+
const depth = schemaFile.split("/").length - 1;
|
|
3478
|
+
const climb = depth === 0 ? "./" : "../".repeat(depth);
|
|
3479
|
+
return `${climb}${SCHEMA_SHAPE_BASENAME}.js`;
|
|
3480
|
+
}
|
|
3481
|
+
function renderEnvModule(schemaFile, inject = false) {
|
|
3482
|
+
const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly. `snapshot` lets load() resolve in a bundled or\n// serverless runtime where no penv.config.ts is on disk.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid. `snapshot` lets load()\n// resolve in a bundled or serverless runtime where no penv.config.ts is on disk.\n";
|
|
3483
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true, snapshot });\n" : "export const env = load(schema, { snapshot });\n";
|
|
3484
|
+
return `import { load } from "@penvhq/penv";
|
|
3485
|
+
import { schema } from "${shapeSpecifier(schemaFile)}";
|
|
3486
|
+
import { snapshot } from "${snapshotSpecifier(schemaFile)}";
|
|
3487
|
+
|
|
3488
|
+
// Re-exported so type-only consumers can import the shape through @env without
|
|
3489
|
+
// triggering config loading \u2014 the shape itself lives in ${import_core16.SCHEMA_SHAPE_FILE}.
|
|
3490
|
+
export { schema };
|
|
3491
|
+
|
|
3492
|
+
` + loadComment + loadCall;
|
|
3493
|
+
}
|
|
3254
3494
|
function renderEnvironments(decisions) {
|
|
3255
3495
|
const shared = " // Environments are a whitelist. A filename segment is an environment only if\n // it is declared here \u2014 penv never infers one from a folder or a filename.\n";
|
|
3256
3496
|
if (decisions.environments.length === 0) {
|
|
@@ -3258,7 +3498,7 @@ function renderEnvironments(decisions) {
|
|
|
3258
3498
|
// can read off your codebase, and an environment you do not have is worse
|
|
3259
3499
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
3260
3500
|
// environments: ["development", "production"],
|
|
3261
|
-
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-
|
|
3501
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
|
|
3262
3502
|
environments: [],
|
|
3263
3503
|
|
|
3264
3504
|
providers: {},
|
|
@@ -3276,7 +3516,7 @@ ${providers} },
|
|
|
3276
3516
|
}
|
|
3277
3517
|
function renderConfigModule(decisions) {
|
|
3278
3518
|
let body = renderEnvironments(decisions);
|
|
3279
|
-
if (decisions.schemaFile !==
|
|
3519
|
+
if (decisions.schemaFile !== import_core16.DEFAULT_SCHEMA_FILE) {
|
|
3280
3520
|
body += `
|
|
3281
3521
|
// The module that exports the schema. It is yours \u2014 penv scaffolds it once
|
|
3282
3522
|
// and never regenerates it \u2014 so this says where you keep it.
|
|
@@ -3299,7 +3539,7 @@ ${body}});
|
|
|
3299
3539
|
`;
|
|
3300
3540
|
}
|
|
3301
3541
|
function renderGitignore(decisions) {
|
|
3302
|
-
const inside = (0,
|
|
3542
|
+
const inside = (0, import_core16.schemaInsideTree)(configOf(decisions));
|
|
3303
3543
|
const listed = inside === void 0 ? "" : `${inside}, `;
|
|
3304
3544
|
return `# Written by penv. Value files hold configuration values and are never
|
|
3305
3545
|
# committed; only the structure, ${listed}meta, and config are.
|
|
@@ -3444,13 +3684,13 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
3444
3684
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
3445
3685
|
}
|
|
3446
3686
|
function shapeError(what, target, alias) {
|
|
3447
|
-
return new
|
|
3687
|
+
return new import_core16.PenvError(
|
|
3448
3688
|
"TSCONFIG_SHAPE",
|
|
3449
3689
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
3450
3690
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
3451
3691
|
);
|
|
3452
3692
|
}
|
|
3453
|
-
function insertEnvAlias(source, target =
|
|
3693
|
+
function insertEnvAlias(source, target = import_core16.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
|
|
3454
3694
|
const alias = `"${name}": ["${target}"]`;
|
|
3455
3695
|
const root = skipTrivia(source, 0);
|
|
3456
3696
|
if (source.charAt(root) !== "{") {
|
|
@@ -3514,46 +3754,102 @@ function renderTsconfig(target, alias) {
|
|
|
3514
3754
|
`;
|
|
3515
3755
|
}
|
|
3516
3756
|
function ensurePenvDir(root) {
|
|
3517
|
-
const dir = (0,
|
|
3518
|
-
if ((0,
|
|
3757
|
+
const dir = (0, import_node_path8.resolve)(root, PENV_DIR2);
|
|
3758
|
+
if ((0, import_node_fs6.existsSync)(dir)) {
|
|
3519
3759
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
3520
3760
|
}
|
|
3521
|
-
(0,
|
|
3761
|
+
(0, import_node_fs6.mkdirSync)(dir, { recursive: true });
|
|
3522
3762
|
return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
|
|
3523
3763
|
}
|
|
3524
|
-
function
|
|
3525
|
-
const file = (0,
|
|
3526
|
-
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)) {
|
|
3527
3767
|
return {
|
|
3528
3768
|
target: "schema",
|
|
3529
3769
|
action: "kept",
|
|
3530
|
-
text: `Kept ${
|
|
3770
|
+
text: `Kept ${import_core16.SCHEMA_SHAPE_FILE}`,
|
|
3531
3771
|
note: "(yours \u2014 penv never regenerates it)"
|
|
3532
3772
|
};
|
|
3533
3773
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3774
|
+
const oldLayout = selfContainedSchemaModule(root, decisions.schemaFile);
|
|
3775
|
+
if (oldLayout !== void 0) {
|
|
3776
|
+
return {
|
|
3777
|
+
target: "schema",
|
|
3778
|
+
action: "conflicted",
|
|
3779
|
+
text: `Kept your schema in ${oldLayout}`,
|
|
3780
|
+
note: `(the shape lives there, from before the ${import_core16.SCHEMA_SHAPE_FILE} split \u2014 penv did not add a second one. To adopt the split: move the \`z.object\` (and any \`declare module\` block) into ${import_core16.SCHEMA_SHAPE_FILE}, leaving ${oldLayout} importing \`schema\` from it and calling \`load\`.)`
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
(0, import_node_fs6.writeFileSync)(file, renderSchemaShapeModule(fields, draft), "utf8");
|
|
3536
3784
|
return {
|
|
3537
3785
|
target: "schema",
|
|
3538
3786
|
action: "created",
|
|
3787
|
+
text: `Generated ${import_core16.SCHEMA_SHAPE_FILE}`,
|
|
3788
|
+
note: draft ? "(draft schema \u2014 review it, it's yours)" : "(the shape \u2014 yours to edit)"
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
|
|
3792
|
+
const file = (0, import_node_path8.join)(root, ...decisions.schemaFile.split("/"));
|
|
3793
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
3794
|
+
return {
|
|
3795
|
+
target: "env",
|
|
3796
|
+
action: "kept",
|
|
3797
|
+
text: `Kept ${decisions.schemaFile}`,
|
|
3798
|
+
note: "(yours \u2014 penv never regenerates it)"
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
|
|
3802
|
+
(0, import_node_fs6.writeFileSync)(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
|
|
3803
|
+
return {
|
|
3804
|
+
target: "env",
|
|
3805
|
+
action: "created",
|
|
3539
3806
|
text: `Generated ${decisions.schemaFile}`,
|
|
3540
|
-
note:
|
|
3807
|
+
note: "(loads the shape \u2014 yours to edit)"
|
|
3541
3808
|
};
|
|
3542
3809
|
}
|
|
3543
3810
|
function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
3544
|
-
const file = (0,
|
|
3545
|
-
if ((0,
|
|
3811
|
+
const file = (0, import_node_path8.join)(root, CONFIG_FILE);
|
|
3812
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
3546
3813
|
return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
|
|
3547
3814
|
}
|
|
3548
|
-
(0,
|
|
3815
|
+
(0, import_node_fs6.writeFileSync)(file, renderConfigModule(decisions), "utf8");
|
|
3549
3816
|
return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
|
|
3550
3817
|
}
|
|
3818
|
+
function snapshotConfig(decisions) {
|
|
3819
|
+
const providers = {};
|
|
3820
|
+
for (const environment of decisions.environments) {
|
|
3821
|
+
providers[environment] = { type: "@penvhq/provider-filesystem" };
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
environments: decisions.environments,
|
|
3825
|
+
providers,
|
|
3826
|
+
...decisions.schemaFile === import_core16.DEFAULT_SCHEMA_FILE ? {} : { schemaFile: decisions.schemaFile },
|
|
3827
|
+
...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: decisions.publicPrefixes }
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
function writeSnapshotModule(root, decisions = DEFAULT_DECISIONS) {
|
|
3831
|
+
const file = (0, import_node_path8.join)(root, SNAPSHOT_FILE);
|
|
3832
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
3833
|
+
return { target: "snapshot", action: "kept", text: `Kept ${SNAPSHOT_FILE}` };
|
|
3834
|
+
}
|
|
3835
|
+
(0, import_node_fs6.writeFileSync)(
|
|
3836
|
+
file,
|
|
3837
|
+
renderSnapshotModule(buildSnapshotFrom(root, snapshotConfig(decisions))),
|
|
3838
|
+
"utf8"
|
|
3839
|
+
);
|
|
3840
|
+
return {
|
|
3841
|
+
target: "snapshot",
|
|
3842
|
+
action: "created",
|
|
3843
|
+
text: `Generated ${SNAPSHOT_FILE}`,
|
|
3844
|
+
note: "(committed \u2014 the bundle resolves load() from it)"
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3551
3847
|
function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
3552
3848
|
const alias = decisions.alias;
|
|
3553
3849
|
const imports = alias.startsWith(IMPORTS_PREFIX);
|
|
3554
|
-
const file = (0,
|
|
3850
|
+
const file = (0, import_node_path8.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
|
|
3555
3851
|
const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
|
|
3556
|
-
if (!(0,
|
|
3852
|
+
if (!(0, import_node_fs6.existsSync)(file)) {
|
|
3557
3853
|
if (imports) {
|
|
3558
3854
|
return {
|
|
3559
3855
|
target: "tsconfig",
|
|
@@ -3562,14 +3858,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3562
3858
|
note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
|
|
3563
3859
|
};
|
|
3564
3860
|
}
|
|
3565
|
-
(0,
|
|
3861
|
+
(0, import_node_fs6.writeFileSync)(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
|
|
3566
3862
|
return {
|
|
3567
3863
|
target: "tsconfig",
|
|
3568
3864
|
action: "created",
|
|
3569
3865
|
text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
|
|
3570
3866
|
};
|
|
3571
3867
|
}
|
|
3572
|
-
const source = (0,
|
|
3868
|
+
const source = (0, import_node_fs6.readFileSync)(file, "utf8");
|
|
3573
3869
|
const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
|
|
3574
3870
|
if (edit.conflict !== void 0) {
|
|
3575
3871
|
return {
|
|
@@ -3586,7 +3882,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3586
3882
|
text: `Kept the ${alias} alias in ${where}`
|
|
3587
3883
|
};
|
|
3588
3884
|
}
|
|
3589
|
-
(0,
|
|
3885
|
+
(0, import_node_fs6.writeFileSync)(file, edit.source, "utf8");
|
|
3590
3886
|
return {
|
|
3591
3887
|
target: "tsconfig",
|
|
3592
3888
|
action: "updated",
|
|
@@ -3594,15 +3890,15 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3594
3890
|
};
|
|
3595
3891
|
}
|
|
3596
3892
|
function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
3597
|
-
const file = (0,
|
|
3893
|
+
const file = (0, import_node_path8.join)(root, PENV_DIR2, GITIGNORE_FILE);
|
|
3598
3894
|
const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
|
|
3599
3895
|
const wanted = renderGitignore(decisions);
|
|
3600
|
-
const existing = (0,
|
|
3896
|
+
const existing = (0, import_node_fs6.existsSync)(file) ? (0, import_node_fs6.readFileSync)(file, "utf8") : void 0;
|
|
3601
3897
|
if (existing === wanted) {
|
|
3602
3898
|
return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
|
|
3603
3899
|
}
|
|
3604
|
-
(0,
|
|
3605
|
-
(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");
|
|
3606
3902
|
return {
|
|
3607
3903
|
target: "gitignore",
|
|
3608
3904
|
action: existing === void 0 ? "created" : "updated",
|
|
@@ -3618,12 +3914,12 @@ function outdatedRuntimeWarning(root) {
|
|
|
3618
3914
|
return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
|
|
3619
3915
|
}
|
|
3620
3916
|
function installedPenvVersion(root) {
|
|
3621
|
-
const file = (0,
|
|
3622
|
-
if (!(0,
|
|
3917
|
+
const file = (0, import_node_path8.join)(root, "node_modules", "@penvhq", "penv", "package.json");
|
|
3918
|
+
if (!(0, import_node_fs6.existsSync)(file)) {
|
|
3623
3919
|
return void 0;
|
|
3624
3920
|
}
|
|
3625
3921
|
try {
|
|
3626
|
-
const version = JSON.parse((0,
|
|
3922
|
+
const version = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8")).version;
|
|
3627
3923
|
return typeof version === "string" ? version : void 0;
|
|
3628
3924
|
} catch {
|
|
3629
3925
|
return void 0;
|
|
@@ -3663,11 +3959,11 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramew
|
|
|
3663
3959
|
};
|
|
3664
3960
|
}
|
|
3665
3961
|
const alsoNote = writeAlso(root, seam.also);
|
|
3666
|
-
const file = (0,
|
|
3962
|
+
const file = (0, import_node_path8.join)(root, ...seam.file.split("/"));
|
|
3667
3963
|
const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
|
|
3668
3964
|
const notes = baseNotes.length === 0 ? "" : `
|
|
3669
3965
|
${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
3670
|
-
if ((0,
|
|
3966
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
3671
3967
|
return {
|
|
3672
3968
|
target: "seam",
|
|
3673
3969
|
action: "info",
|
|
@@ -3675,8 +3971,8 @@ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
|
3675
3971
|
note: withWarning(`${seam.ifPresent}${notes}`, outdated)
|
|
3676
3972
|
};
|
|
3677
3973
|
}
|
|
3678
|
-
(0,
|
|
3679
|
-
(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");
|
|
3680
3976
|
return {
|
|
3681
3977
|
target: "seam",
|
|
3682
3978
|
action: outdated === void 0 ? "created" : "info",
|
|
@@ -3688,12 +3984,12 @@ function writeAlso(root, also) {
|
|
|
3688
3984
|
if (also === void 0) {
|
|
3689
3985
|
return void 0;
|
|
3690
3986
|
}
|
|
3691
|
-
const file = (0,
|
|
3692
|
-
if ((0,
|
|
3987
|
+
const file = (0, import_node_path8.join)(root, ...also.file.split("/"));
|
|
3988
|
+
if ((0, import_node_fs6.existsSync)(file)) {
|
|
3693
3989
|
return also.ifPresent;
|
|
3694
3990
|
}
|
|
3695
|
-
(0,
|
|
3696
|
-
(0,
|
|
3991
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path8.dirname)(file), { recursive: true });
|
|
3992
|
+
(0, import_node_fs6.writeFileSync)(file, also.content, "utf8");
|
|
3697
3993
|
return `Wrote ${also.file} to register it.`;
|
|
3698
3994
|
}
|
|
3699
3995
|
function withWarning(note, warning) {
|
|
@@ -3706,8 +4002,15 @@ function withWarning(note, warning) {
|
|
|
3706
4002
|
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3707
4003
|
const steps = [
|
|
3708
4004
|
ensurePenvDir(root),
|
|
3709
|
-
|
|
4005
|
+
// The shape at the root, then the wrapper that loads it — both write-once.
|
|
4006
|
+
// The shape step needs the decisions too: an old-layout `schemaFile` already
|
|
4007
|
+
// holds the shape, and a second `penv.schema.ts` beside it would not compile.
|
|
4008
|
+
writeSchemaShapeFile(root, fields, draft, decisions),
|
|
4009
|
+
writeEnvFile(root, decisions),
|
|
3710
4010
|
writeConfigFile(root, decisions),
|
|
4011
|
+
// After the config, whose evaluated shape the snapshot embeds; the wrapper
|
|
4012
|
+
// env.ts written above already imports it.
|
|
4013
|
+
writeSnapshotModule(root, decisions),
|
|
3711
4014
|
writeTsconfigAlias(root, decisions),
|
|
3712
4015
|
writeGitignore(root, decisions)
|
|
3713
4016
|
];
|
|
@@ -3715,7 +4018,7 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
|
|
|
3715
4018
|
return seam === void 0 ? steps : [...steps, seam];
|
|
3716
4019
|
}
|
|
3717
4020
|
function runInit(options) {
|
|
3718
|
-
const root = (0,
|
|
4021
|
+
const root = (0, import_node_path8.resolve)(options.cwd);
|
|
3719
4022
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
3720
4023
|
return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
|
|
3721
4024
|
}
|
|
@@ -3724,10 +4027,12 @@ function renderInit(result2) {
|
|
|
3724
4027
|
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3725
4028
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3726
4029
|
});
|
|
4030
|
+
const schemaStep = result2.steps.find((step) => step.target === "schema");
|
|
4031
|
+
const shapeFile = schemaStep?.action === "conflicted" ? result2.decisions.schemaFile : import_core16.SCHEMA_SHAPE_FILE;
|
|
3727
4032
|
return [
|
|
3728
4033
|
...formatSteps(steps),
|
|
3729
4034
|
"",
|
|
3730
|
-
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${
|
|
4035
|
+
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${shapeFile}, then:`,
|
|
3731
4036
|
tip(out.cyan("penv set <key>")),
|
|
3732
4037
|
...result2.decisions.environments.length === 0 ? [
|
|
3733
4038
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
@@ -3755,7 +4060,7 @@ var initCommand = (0, import_citty9.defineCommand)({
|
|
|
3755
4060
|
},
|
|
3756
4061
|
schema: {
|
|
3757
4062
|
type: "string",
|
|
3758
|
-
description: `Where the schema module goes, e.g. src/env.ts (default: ${
|
|
4063
|
+
description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core16.DEFAULT_SCHEMA_FILE})`
|
|
3759
4064
|
},
|
|
3760
4065
|
alias: {
|
|
3761
4066
|
type: "string",
|
|
@@ -3768,7 +4073,7 @@ var initCommand = (0, import_citty9.defineCommand)({
|
|
|
3768
4073
|
},
|
|
3769
4074
|
run({ args }) {
|
|
3770
4075
|
return guard(async () => {
|
|
3771
|
-
const root = (0,
|
|
4076
|
+
const root = (0, import_node_path8.resolve)(process.cwd());
|
|
3772
4077
|
const environments = environmentsFromFlag(args.env);
|
|
3773
4078
|
const plan2 = planInit(root, {
|
|
3774
4079
|
...args.schema === void 0 ? {} : { schema: args.schema },
|
|
@@ -3817,7 +4122,7 @@ function inferType(value) {
|
|
|
3817
4122
|
}
|
|
3818
4123
|
function draftFields(entries) {
|
|
3819
4124
|
return entries.map((entry) => {
|
|
3820
|
-
const key = (0,
|
|
4125
|
+
const key = (0, import_core17.accessPath)((0, import_core17.refFromVariable)(entry.key)).join(".");
|
|
3821
4126
|
return {
|
|
3822
4127
|
key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
|
|
3823
4128
|
type: inferType(entry.value)
|
|
@@ -3828,33 +4133,33 @@ function assertImportable(ref, variable) {
|
|
|
3828
4133
|
if (!ref.name.includes(".")) {
|
|
3829
4134
|
return;
|
|
3830
4135
|
}
|
|
3831
|
-
throw new
|
|
4136
|
+
throw new import_core17.PenvError(
|
|
3832
4137
|
"IMPORT_UNPARSEABLE_NAME",
|
|
3833
4138
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
3834
4139
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
3835
4140
|
);
|
|
3836
4141
|
}
|
|
3837
4142
|
function assertNotReserved(ref, variable, where, config) {
|
|
3838
|
-
if ((0,
|
|
3839
|
-
throw new
|
|
4143
|
+
if ((0, import_core17.isReservedToken)(ref.name, config)) {
|
|
4144
|
+
throw new import_core17.ReservedTokenError("parameter", variable, where);
|
|
3840
4145
|
}
|
|
3841
4146
|
}
|
|
3842
4147
|
function assertRoundTrips(ref, variable, config) {
|
|
3843
|
-
if ((0,
|
|
4148
|
+
if ((0, import_core17.roundTripsCleanly)(variable)) {
|
|
3844
4149
|
return;
|
|
3845
4150
|
}
|
|
3846
|
-
if ((0,
|
|
4151
|
+
if ((0, import_core17.variableName)(ref, config) === variable) {
|
|
3847
4152
|
return;
|
|
3848
4153
|
}
|
|
3849
|
-
const generated = (0,
|
|
3850
|
-
throw new
|
|
4154
|
+
const generated = (0, import_core17.variableName)(ref, config);
|
|
4155
|
+
throw new import_core17.PenvError(
|
|
3851
4156
|
"IMPORT_LOSSY_NAME",
|
|
3852
4157
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
3853
4158
|
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`override\` block of penv.config.ts \u2014 \`override: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
|
|
3854
4159
|
);
|
|
3855
4160
|
}
|
|
3856
4161
|
function collisionsIn(refs, config) {
|
|
3857
|
-
const errors = (0,
|
|
4162
|
+
const errors = (0, import_core17.checkNameCollisions)(refs, config);
|
|
3858
4163
|
const first = errors[0];
|
|
3859
4164
|
if (first !== void 0) {
|
|
3860
4165
|
throw first;
|
|
@@ -3864,10 +4169,10 @@ function assertDeclared(segment, config) {
|
|
|
3864
4169
|
if (config.environments.includes(segment)) {
|
|
3865
4170
|
return segment;
|
|
3866
4171
|
}
|
|
3867
|
-
throw new
|
|
4172
|
+
throw new import_core17.UnknownEnvironmentError(segment, config.environments);
|
|
3868
4173
|
}
|
|
3869
4174
|
function scopeFromFilename(file, config) {
|
|
3870
|
-
const name = (0,
|
|
4175
|
+
const name = (0, import_node_path9.basename)(file);
|
|
3871
4176
|
const segments = name.split(".");
|
|
3872
4177
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
3873
4178
|
if (start === -1) {
|
|
@@ -3889,13 +4194,13 @@ function scopeFromFilename(file, config) {
|
|
|
3889
4194
|
return { kind: "environment-local", environment: assertDeclared(first, config) };
|
|
3890
4195
|
}
|
|
3891
4196
|
if (first === LOCAL && rest.length === 2) {
|
|
3892
|
-
throw new
|
|
4197
|
+
throw new import_core17.FilenameGrammarError(
|
|
3893
4198
|
name,
|
|
3894
4199
|
"`local` precedes the environment segment",
|
|
3895
4200
|
`The environment segment always precedes \`local\` \u2014 \`.env.${second}.${LOCAL}\` is the file Next.js and Vite read, and \`.env.${LOCAL}.${second}\` is not a synonym for it. Rename it to \`.env.${second}.${LOCAL}\`, then import it again. Nothing was imported.`
|
|
3896
4201
|
);
|
|
3897
4202
|
}
|
|
3898
|
-
throw new
|
|
4203
|
+
throw new import_core17.FilenameGrammarError(
|
|
3899
4204
|
name,
|
|
3900
4205
|
`\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
|
|
3901
4206
|
"A dotenv file carries exactly one scope: `.env`, `.env.<environment>`, `.env.local`, or `.env.<environment>.local`. Point `penv import` at one of those. Nothing was imported."
|
|
@@ -3910,7 +4215,7 @@ function environmentOf(scope) {
|
|
|
3910
4215
|
case "local":
|
|
3911
4216
|
return void 0;
|
|
3912
4217
|
default:
|
|
3913
|
-
return (0,
|
|
4218
|
+
return (0, import_core17.assertNever)(scope, "scope");
|
|
3914
4219
|
}
|
|
3915
4220
|
}
|
|
3916
4221
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -3923,7 +4228,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
3923
4228
|
case "environment-local":
|
|
3924
4229
|
return scope;
|
|
3925
4230
|
default:
|
|
3926
|
-
return (0,
|
|
4231
|
+
return (0, import_core17.assertNever)(scope, "scope");
|
|
3927
4232
|
}
|
|
3928
4233
|
}
|
|
3929
4234
|
function explicitEnvironment(options, source, config) {
|
|
@@ -3931,7 +4236,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
3931
4236
|
if (value.length > 0) {
|
|
3932
4237
|
return value;
|
|
3933
4238
|
}
|
|
3934
|
-
throw new
|
|
4239
|
+
throw new import_core17.PenvError(
|
|
3935
4240
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
3936
4241
|
`\`--env\` for the import of ${source} names no environment`,
|
|
3937
4242
|
`Pass a declared environment \u2014 ${config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
|
|
@@ -3941,7 +4246,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
3941
4246
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
3942
4247
|
return;
|
|
3943
4248
|
}
|
|
3944
|
-
throw new
|
|
4249
|
+
throw new import_core17.PenvError(
|
|
3945
4250
|
"IMPORT_ENV_CONFLICT",
|
|
3946
4251
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
3947
4252
|
`Drop \`--env\` to import ${source} as ${derived}, pass \`--env ${derived}\` to say the same thing twice, or point \`penv import\` at the file that holds ${explicit}'s values. Nothing was imported.`
|
|
@@ -3951,7 +4256,7 @@ function environmentNamed(file, explicit) {
|
|
|
3951
4256
|
if (explicit !== void 0 && explicit.trim().length > 0) {
|
|
3952
4257
|
return explicit.trim();
|
|
3953
4258
|
}
|
|
3954
|
-
const segments = (0,
|
|
4259
|
+
const segments = (0, import_node_path9.basename)(file).split(".");
|
|
3955
4260
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
3956
4261
|
if (start === -1) {
|
|
3957
4262
|
return void 0;
|
|
@@ -3965,15 +4270,15 @@ function decisionsOf(config, cwd) {
|
|
|
3965
4270
|
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
3966
4271
|
// choice, so it is not turned on here.
|
|
3967
4272
|
inject: false,
|
|
3968
|
-
schemaFile: (0,
|
|
4273
|
+
schemaFile: (0, import_core17.schemaFileOf)(config),
|
|
3969
4274
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3970
4275
|
alias: detectAlias(cwd)
|
|
3971
4276
|
};
|
|
3972
4277
|
}
|
|
3973
4278
|
function configInEffect(cwd, environment) {
|
|
3974
|
-
const existing = (0,
|
|
4279
|
+
const existing = (0, import_core17.findConfigFile)(cwd);
|
|
3975
4280
|
if (existing !== void 0) {
|
|
3976
|
-
const config = (0,
|
|
4281
|
+
const config = (0, import_core17.loadConfigFrom)(existing);
|
|
3977
4282
|
return { config, decisions: decisionsOf(config, cwd) };
|
|
3978
4283
|
}
|
|
3979
4284
|
const planned = planInit(cwd).decisions;
|
|
@@ -3985,16 +4290,16 @@ function configInEffect(cwd, environment) {
|
|
|
3985
4290
|
return { config: openProject(cwd).config, decisions };
|
|
3986
4291
|
}
|
|
3987
4292
|
function importDotenv(options) {
|
|
3988
|
-
const cwd = (0,
|
|
3989
|
-
const file = (0,
|
|
3990
|
-
if (!(0,
|
|
3991
|
-
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(
|
|
3992
4297
|
"IMPORT_FILE_MISSING",
|
|
3993
4298
|
`There is no file at ${file} to import`,
|
|
3994
4299
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
3995
4300
|
);
|
|
3996
4301
|
}
|
|
3997
|
-
const parsed = (0,
|
|
4302
|
+
const parsed = (0, import_core17.parseDotenv)((0, import_node_fs7.readFileSync)(file, "utf8"));
|
|
3998
4303
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
3999
4304
|
const source = displayPath2(cwd, file);
|
|
4000
4305
|
const named = scopeFromFilename(file, config);
|
|
@@ -4002,10 +4307,10 @@ function importDotenv(options) {
|
|
|
4002
4307
|
const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
|
|
4003
4308
|
assertEnvironmentAgrees(derived, explicit, source);
|
|
4004
4309
|
const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
|
|
4005
|
-
const environment = (0,
|
|
4310
|
+
const environment = (0, import_core17.lookupEnvironment)(config, explicit ?? derived);
|
|
4006
4311
|
const refs = [];
|
|
4007
4312
|
for (const entry of parsed.entries) {
|
|
4008
|
-
const ref = (0,
|
|
4313
|
+
const ref = (0, import_core17.refFromVariable)(entry.key);
|
|
4009
4314
|
assertImportable(ref, entry.key);
|
|
4010
4315
|
assertNotReserved(ref, entry.key, source, config);
|
|
4011
4316
|
assertRoundTrips(ref, entry.key, config);
|
|
@@ -4031,7 +4336,8 @@ function importDotenv(options) {
|
|
|
4031
4336
|
}
|
|
4032
4337
|
}
|
|
4033
4338
|
const backup = `${file}${BACKUP_SUFFIX}`;
|
|
4034
|
-
(0,
|
|
4339
|
+
(0, import_node_fs7.copyFileSync)(file, backup);
|
|
4340
|
+
refreshSnapshot(project);
|
|
4035
4341
|
return {
|
|
4036
4342
|
root: project.root,
|
|
4037
4343
|
file,
|
|
@@ -4045,7 +4351,7 @@ function importDotenv(options) {
|
|
|
4045
4351
|
};
|
|
4046
4352
|
}
|
|
4047
4353
|
function displayPath2(root, file) {
|
|
4048
|
-
const rel = (0,
|
|
4354
|
+
const rel = (0, import_node_path9.relative)(root, file);
|
|
4049
4355
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
4050
4356
|
}
|
|
4051
4357
|
function keptSchemaStep(step, variables) {
|
|
@@ -4133,7 +4439,7 @@ var importCommand = (0, import_citty10.defineCommand)({
|
|
|
4133
4439
|
|
|
4134
4440
|
// src/commands/key.ts
|
|
4135
4441
|
var import_node_crypto = require("crypto");
|
|
4136
|
-
var
|
|
4442
|
+
var import_core18 = require("@penvhq/core");
|
|
4137
4443
|
var import_citty11 = require("citty");
|
|
4138
4444
|
|
|
4139
4445
|
// src/keychain.ts
|
|
@@ -4167,35 +4473,35 @@ function runKeyCreate(options) {
|
|
|
4167
4473
|
const environment = targetEnvironment(project, options.environment);
|
|
4168
4474
|
const declared = project.config.keys?.[environment];
|
|
4169
4475
|
if (declared === void 0) {
|
|
4170
|
-
throw new
|
|
4476
|
+
throw new import_core18.PenvError(
|
|
4171
4477
|
"KEY_SOURCE_UNDECLARED",
|
|
4172
4478
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
4173
4479
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
4174
4480
|
);
|
|
4175
4481
|
}
|
|
4176
|
-
const key = (0, import_node_crypto.randomBytes)(
|
|
4482
|
+
const key = (0, import_node_crypto.randomBytes)(import_core18.KEY_BYTES).toString("base64");
|
|
4177
4483
|
if (declared.source === "keychain") {
|
|
4178
4484
|
const keychain = options.keychain ?? defaultKeychain;
|
|
4179
4485
|
if (options.force !== true) {
|
|
4180
4486
|
let existing;
|
|
4181
4487
|
try {
|
|
4182
|
-
existing = keychain.getPassword(
|
|
4488
|
+
existing = keychain.getPassword(import_core18.KEYCHAIN_SERVICE, declared.id);
|
|
4183
4489
|
} catch (cause) {
|
|
4184
|
-
throw new
|
|
4490
|
+
throw new import_core18.PenvError(
|
|
4185
4491
|
"KEYCHAIN_UNAVAILABLE",
|
|
4186
4492
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
4187
4493
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
4188
4494
|
);
|
|
4189
4495
|
}
|
|
4190
4496
|
if (existing !== null) {
|
|
4191
|
-
throw new
|
|
4497
|
+
throw new import_core18.PenvError(
|
|
4192
4498
|
"KEY_EXISTS",
|
|
4193
4499
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
4194
4500
|
"Replacing it would orphan every value already sealed under it \u2014 they could never be decrypted again. Re-run with `--force` only if you are certain nothing is sealed under the current key."
|
|
4195
4501
|
);
|
|
4196
4502
|
}
|
|
4197
4503
|
}
|
|
4198
|
-
keychain.setPassword(
|
|
4504
|
+
keychain.setPassword(import_core18.KEYCHAIN_SERVICE, declared.id, key);
|
|
4199
4505
|
return { source: "keychain", environment, id: declared.id };
|
|
4200
4506
|
}
|
|
4201
4507
|
return {
|
|
@@ -4263,7 +4569,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
|
|
|
4263
4569
|
});
|
|
4264
4570
|
|
|
4265
4571
|
// src/commands/list.ts
|
|
4266
|
-
var
|
|
4572
|
+
var import_core19 = require("@penvhq/core");
|
|
4267
4573
|
var import_citty12 = require("citty");
|
|
4268
4574
|
function scopeLabel2(scope) {
|
|
4269
4575
|
switch (scope.kind) {
|
|
@@ -4276,7 +4582,7 @@ function scopeLabel2(scope) {
|
|
|
4276
4582
|
case "unscoped":
|
|
4277
4583
|
return "default";
|
|
4278
4584
|
default:
|
|
4279
|
-
return (0,
|
|
4585
|
+
return (0, import_core19.assertNever)(scope, "scope");
|
|
4280
4586
|
}
|
|
4281
4587
|
}
|
|
4282
4588
|
async function runList(options) {
|
|
@@ -4284,12 +4590,12 @@ async function runList(options) {
|
|
|
4284
4590
|
const environment = targetEnvironment(project, options.environment);
|
|
4285
4591
|
const keys = keySourceFor(project, environment);
|
|
4286
4592
|
const parameters = [];
|
|
4287
|
-
for (const resolution of await (0,
|
|
4593
|
+
for (const resolution of await (0, import_core19.resolveAll)(environment, project.provider, keys)) {
|
|
4288
4594
|
const winner = resolution.winner;
|
|
4289
4595
|
const scope = winner?.file.scope;
|
|
4290
4596
|
parameters.push({
|
|
4291
4597
|
parameter: resolution.parameter,
|
|
4292
|
-
variable: (0,
|
|
4598
|
+
variable: (0, import_core19.variableName)(resolution.ref, project.config),
|
|
4293
4599
|
scope: scope === void 0 ? "absent" : scopeLabel2(scope),
|
|
4294
4600
|
location: winner?.location,
|
|
4295
4601
|
encrypted: winner?.file.encrypted === true,
|
|
@@ -4342,7 +4648,7 @@ var listCommand = (0, import_citty12.defineCommand)({
|
|
|
4342
4648
|
});
|
|
4343
4649
|
|
|
4344
4650
|
// src/commands/mv.ts
|
|
4345
|
-
var
|
|
4651
|
+
var import_core20 = require("@penvhq/core");
|
|
4346
4652
|
var import_citty13 = require("citty");
|
|
4347
4653
|
function environmentOf2(file) {
|
|
4348
4654
|
const scope = file.scope;
|
|
@@ -4358,39 +4664,39 @@ async function planFile(project, source, target, parameter) {
|
|
|
4358
4664
|
}
|
|
4359
4665
|
const environment = environmentOf2(source);
|
|
4360
4666
|
if (environment === void 0) {
|
|
4361
|
-
throw new
|
|
4667
|
+
throw new import_core20.PenvError(
|
|
4362
4668
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
4363
|
-
`${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`,
|
|
4364
4670
|
"Keys are declared per environment in the `keys` block of penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
|
|
4365
4671
|
);
|
|
4366
4672
|
}
|
|
4367
4673
|
const keys = keySourceFor(project, environment);
|
|
4368
|
-
const opened = (0,
|
|
4674
|
+
const opened = (0, import_core20.openValue)(source, stored, keys);
|
|
4369
4675
|
if (opened.kind === "failed") {
|
|
4370
|
-
throw new
|
|
4676
|
+
throw new import_core20.PenvError(
|
|
4371
4677
|
"VALUE_UNDECRYPTABLE",
|
|
4372
|
-
`${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}`,
|
|
4373
4679
|
"A sealed value is bound to the file it lives in, so moving it means opening it and sealing it again. Make the key available and run this again. Nothing has been moved."
|
|
4374
4680
|
);
|
|
4375
4681
|
}
|
|
4376
4682
|
return {
|
|
4377
4683
|
source,
|
|
4378
4684
|
target,
|
|
4379
|
-
contents: (0,
|
|
4685
|
+
contents: (0, import_core20.sealValue)(target, opened.value, keys, parameter, environment),
|
|
4380
4686
|
resealed: true
|
|
4381
4687
|
};
|
|
4382
4688
|
}
|
|
4383
4689
|
function filesOf(all, ref) {
|
|
4384
|
-
const id = (0,
|
|
4385
|
-
return all.filter((file) => (0,
|
|
4690
|
+
const id = (0, import_core20.parameterId)(ref);
|
|
4691
|
+
return all.filter((file) => (0, import_core20.parameterId)(file) === id);
|
|
4386
4692
|
}
|
|
4387
4693
|
async function runMove(options) {
|
|
4388
4694
|
const project = openProject(options.cwd);
|
|
4389
4695
|
const from = refFromKey(options.from, project.config);
|
|
4390
4696
|
assertWritableKey(options.to);
|
|
4391
4697
|
const to = refFromKey(options.to, project.config);
|
|
4392
|
-
if ((0,
|
|
4393
|
-
throw new
|
|
4698
|
+
if ((0, import_core20.parameterId)(from) === (0, import_core20.parameterId)(to)) {
|
|
4699
|
+
throw new import_core20.PenvError(
|
|
4394
4700
|
"PARAMETER_UNCHANGED",
|
|
4395
4701
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
4396
4702
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -4400,24 +4706,24 @@ async function runMove(options) {
|
|
|
4400
4706
|
const sources = filesOf(all, from);
|
|
4401
4707
|
const meta = await project.provider.readMeta(from);
|
|
4402
4708
|
if (sources.length === 0 && meta === void 0) {
|
|
4403
|
-
throw new
|
|
4709
|
+
throw new import_core20.PenvError(
|
|
4404
4710
|
"PARAMETER_ABSENT",
|
|
4405
|
-
`Parameter ${(0,
|
|
4711
|
+
`Parameter ${(0, import_core20.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
|
|
4406
4712
|
`\`penv list\` shows every parameter penv holds.`
|
|
4407
4713
|
);
|
|
4408
4714
|
}
|
|
4409
4715
|
const occupied2 = filesOf(all, to);
|
|
4410
4716
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
4411
|
-
throw new
|
|
4717
|
+
throw new import_core20.PenvError(
|
|
4412
4718
|
"PARAMETER_EXISTS",
|
|
4413
|
-
`Parameter ${(0,
|
|
4414
|
-
`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.`
|
|
4415
4721
|
);
|
|
4416
4722
|
}
|
|
4417
4723
|
const planned = [];
|
|
4418
4724
|
for (const source of sources) {
|
|
4419
4725
|
const target = { ...source, namespace: to.namespace, name: to.name };
|
|
4420
|
-
const one = await planFile(project, source, target, (0,
|
|
4726
|
+
const one = await planFile(project, source, target, (0, import_core20.parameterId)(to));
|
|
4421
4727
|
if (one !== void 0) {
|
|
4422
4728
|
planned.push(one);
|
|
4423
4729
|
}
|
|
@@ -4434,19 +4740,21 @@ async function runMove(options) {
|
|
|
4434
4740
|
if (meta !== void 0) {
|
|
4435
4741
|
await project.provider.removeMeta(from);
|
|
4436
4742
|
}
|
|
4743
|
+
refreshSnapshot(project);
|
|
4437
4744
|
return {
|
|
4438
|
-
from: (0,
|
|
4439
|
-
to: (0,
|
|
4745
|
+
from: (0, import_core20.parameterId)(from),
|
|
4746
|
+
to: (0, import_core20.parameterId)(to),
|
|
4440
4747
|
files: planned.map((file) => ({
|
|
4441
|
-
from: (0,
|
|
4442
|
-
to: (0,
|
|
4748
|
+
from: (0, import_core20.formatValueFile)(file.source),
|
|
4749
|
+
to: (0, import_core20.formatValueFile)(file.target),
|
|
4443
4750
|
resealed: file.resealed
|
|
4444
4751
|
})),
|
|
4445
4752
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
4446
4753
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
4447
4754
|
// that printed the first while moving the second names no file on disk.
|
|
4448
|
-
meta: meta === void 0 ? void 0 : (0,
|
|
4449
|
-
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)
|
|
4450
4758
|
};
|
|
4451
4759
|
}
|
|
4452
4760
|
function renderMove(result2) {
|
|
@@ -4463,7 +4771,7 @@ function renderMove(result2) {
|
|
|
4463
4771
|
lines.push(
|
|
4464
4772
|
"",
|
|
4465
4773
|
tip(
|
|
4466
|
-
|
|
4774
|
+
`${result2.schemaFile} still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
|
|
4467
4775
|
)
|
|
4468
4776
|
);
|
|
4469
4777
|
return lines;
|
|
@@ -4490,7 +4798,7 @@ var mvCommand = (0, import_citty13.defineCommand)({
|
|
|
4490
4798
|
});
|
|
4491
4799
|
|
|
4492
4800
|
// src/commands/pull.ts
|
|
4493
|
-
var
|
|
4801
|
+
var import_core21 = require("@penvhq/core");
|
|
4494
4802
|
var import_citty14 = require("citty");
|
|
4495
4803
|
async function pullProjection(project, source, environment) {
|
|
4496
4804
|
const tree = localTree(project);
|
|
@@ -4503,7 +4811,7 @@ async function pullProjection(project, source, environment) {
|
|
|
4503
4811
|
}
|
|
4504
4812
|
let meta = 0;
|
|
4505
4813
|
for (const name of [...names].sort()) {
|
|
4506
|
-
const ref = (0,
|
|
4814
|
+
const ref = (0, import_core21.refFromVariable)(name);
|
|
4507
4815
|
if (tree.readMetaSync(ref) === void 0) {
|
|
4508
4816
|
tree.writeMetaSync(ref, {});
|
|
4509
4817
|
meta += 1;
|
|
@@ -4526,7 +4834,7 @@ async function runPull(options) {
|
|
|
4526
4834
|
if (source.type === LOCAL_TREE_TYPE) {
|
|
4527
4835
|
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
4528
4836
|
}
|
|
4529
|
-
if ((0,
|
|
4837
|
+
if ((0, import_core21.holdsProjection)(source)) {
|
|
4530
4838
|
return pullProjection(project, source, environment);
|
|
4531
4839
|
}
|
|
4532
4840
|
const tree = localTree(project);
|
|
@@ -4550,6 +4858,7 @@ async function runPull(options) {
|
|
|
4550
4858
|
tree.writeMetaSync(ref, block);
|
|
4551
4859
|
meta += 1;
|
|
4552
4860
|
}
|
|
4861
|
+
refreshSnapshot(project);
|
|
4553
4862
|
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
4554
4863
|
}
|
|
4555
4864
|
function renderPull(result2) {
|
|
@@ -4618,7 +4927,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
|
|
|
4618
4927
|
});
|
|
4619
4928
|
|
|
4620
4929
|
// src/commands/remove.ts
|
|
4621
|
-
var
|
|
4930
|
+
var import_core22 = require("@penvhq/core");
|
|
4622
4931
|
var import_citty15 = require("citty");
|
|
4623
4932
|
async function runRemove(options) {
|
|
4624
4933
|
const project = openProject(options.cwd);
|
|
@@ -4636,12 +4945,13 @@ async function runRemove(options) {
|
|
|
4636
4945
|
continue;
|
|
4637
4946
|
}
|
|
4638
4947
|
await project.provider.remove(file);
|
|
4639
|
-
removed.push((0,
|
|
4948
|
+
removed.push((0, import_core22.formatValueFile)(file));
|
|
4640
4949
|
}
|
|
4950
|
+
refreshSnapshot(project);
|
|
4641
4951
|
return {
|
|
4642
4952
|
parameter: options.key,
|
|
4643
4953
|
removed,
|
|
4644
|
-
considered: files.map((file) => (0,
|
|
4954
|
+
considered: files.map((file) => (0, import_core22.formatValueFile)(file))
|
|
4645
4955
|
};
|
|
4646
4956
|
}
|
|
4647
4957
|
function renderRemove(result2) {
|
|
@@ -4690,7 +5000,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
|
|
|
4690
5000
|
});
|
|
4691
5001
|
|
|
4692
5002
|
// src/commands/rotate.ts
|
|
4693
|
-
var
|
|
5003
|
+
var import_core23 = require("@penvhq/core");
|
|
4694
5004
|
var import_citty16 = require("citty");
|
|
4695
5005
|
function rotatingFile(ref, environment) {
|
|
4696
5006
|
return {
|
|
@@ -4710,13 +5020,14 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
4710
5020
|
value,
|
|
4711
5021
|
environment
|
|
4712
5022
|
});
|
|
5023
|
+
refreshSnapshot(project);
|
|
4713
5024
|
return;
|
|
4714
5025
|
}
|
|
4715
5026
|
await provider.write(rotatingFile(ref, environment), value);
|
|
4716
5027
|
}
|
|
4717
5028
|
function requireNewValue(value, phase, key) {
|
|
4718
5029
|
if (value === void 0) {
|
|
4719
|
-
throw new
|
|
5030
|
+
throw new import_core23.PenvError(
|
|
4720
5031
|
"ROTATION_NO_VALUE",
|
|
4721
5032
|
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
4722
5033
|
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
@@ -4725,8 +5036,8 @@ function requireNewValue(value, phase, key) {
|
|
|
4725
5036
|
return value;
|
|
4726
5037
|
}
|
|
4727
5038
|
function requireRetaining(provider, environment) {
|
|
4728
|
-
if (!(0,
|
|
4729
|
-
throw new
|
|
5039
|
+
if (!(0, import_core23.retainsPrevious)(provider)) {
|
|
5040
|
+
throw new import_core23.PenvError(
|
|
4730
5041
|
"ROTATION_NOT_RETAINING",
|
|
4731
5042
|
`A dual-valid rotation needs the previous value to stay readable during the grace window, and the \`${provider.type}\` provider for environment ${environment} does not retain it`,
|
|
4732
5043
|
"Point this environment at a provider that keeps prior versions (its `readPrevious` is what penv reads during the window), or, if a momentary overlap is not required, declare the parameter `atomic-cutover` in its meta and flip it in one step."
|
|
@@ -4739,8 +5050,8 @@ async function runRotate(options) {
|
|
|
4739
5050
|
const environment = targetEnvironment(project, options.environment);
|
|
4740
5051
|
const ref = refFromKey(options.key, project.config);
|
|
4741
5052
|
const source = await sourceProviderFor(project, environment);
|
|
4742
|
-
if (!(0,
|
|
4743
|
-
throw new
|
|
5053
|
+
if (!(0, import_core23.holdsRecords)(source)) {
|
|
5054
|
+
throw new import_core23.PenvError(
|
|
4744
5055
|
"ROTATION_NOT_RECORDS",
|
|
4745
5056
|
`Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
|
|
4746
5057
|
"Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
|
|
@@ -4749,9 +5060,9 @@ async function runRotate(options) {
|
|
|
4749
5060
|
const provider = source;
|
|
4750
5061
|
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4751
5062
|
const before = await provider.readMeta(ref);
|
|
4752
|
-
const { mechanism } = (0,
|
|
5063
|
+
const { mechanism } = (0, import_core23.rotationOf)(before, environment);
|
|
4753
5064
|
if (mechanism === void 0) {
|
|
4754
|
-
throw new
|
|
5065
|
+
throw new import_core23.PenvError(
|
|
4755
5066
|
"ROTATION_NO_MECHANISM",
|
|
4756
5067
|
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
4757
5068
|
'Set `rotationMechanism` in the parameter\'s meta to `"dual-valid"` (a grace-window overlap) or `"atomic-cutover"` (a single flip), then run `penv rotate` again.'
|
|
@@ -4761,7 +5072,7 @@ async function runRotate(options) {
|
|
|
4761
5072
|
const complete = options.complete === true;
|
|
4762
5073
|
if (mechanism === "atomic-cutover") {
|
|
4763
5074
|
if (begin || complete) {
|
|
4764
|
-
throw new
|
|
5075
|
+
throw new import_core23.PenvError(
|
|
4765
5076
|
"ROTATION_MECHANISM_MISMATCH",
|
|
4766
5077
|
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
4767
5078
|
"Run `penv rotate <key> <value>` with no phase flag to flip it. `--begin`/`--complete` bracket a dual-valid grace window, which atomic-cutover has none of."
|
|
@@ -4769,13 +5080,13 @@ async function runRotate(options) {
|
|
|
4769
5080
|
}
|
|
4770
5081
|
const value = requireNewValue(options.value, "cutover", options.key);
|
|
4771
5082
|
await writeRotatedValue(project, provider, ref, environment, value);
|
|
4772
|
-
const after2 = (0,
|
|
5083
|
+
const after2 = (0, import_core23.completeRotation)(before, environment, nowIso);
|
|
4773
5084
|
await provider.writeMeta(ref, after2);
|
|
4774
5085
|
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
4775
5086
|
}
|
|
4776
5087
|
const retaining = requireRetaining(provider, environment);
|
|
4777
5088
|
if (begin === complete) {
|
|
4778
|
-
throw new
|
|
5089
|
+
throw new import_core23.PenvError(
|
|
4779
5090
|
"ROTATION_PHASE_REQUIRED",
|
|
4780
5091
|
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
4781
5092
|
"`--begin` writes the new value and opens the grace window; `--complete` closes it once every reader has moved to the new value. Run them in that order, one at a time."
|
|
@@ -4784,16 +5095,16 @@ async function runRotate(options) {
|
|
|
4784
5095
|
if (begin) {
|
|
4785
5096
|
const value = requireNewValue(options.value, "begin", options.key);
|
|
4786
5097
|
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
4787
|
-
const after2 = (0,
|
|
5098
|
+
const after2 = (0, import_core23.beginRotation)(before, environment, nowIso);
|
|
4788
5099
|
await retaining.writeMeta(ref, after2);
|
|
4789
5100
|
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
4790
5101
|
}
|
|
4791
|
-
const after = (0,
|
|
5102
|
+
const after = (0, import_core23.completeRotation)(before, environment, nowIso);
|
|
4792
5103
|
await retaining.writeMeta(ref, after);
|
|
4793
5104
|
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
4794
5105
|
}
|
|
4795
5106
|
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
4796
|
-
const { state, rotatingSince, lastRotated } = (0,
|
|
5107
|
+
const { state, rotatingSince, lastRotated } = (0, import_core23.rotationOf)(after, environment);
|
|
4797
5108
|
return {
|
|
4798
5109
|
parameter: [...ref.namespace, ref.name].join("/"),
|
|
4799
5110
|
environment,
|
|
@@ -4877,15 +5188,57 @@ var rotateCommand = (0, import_citty16.defineCommand)({
|
|
|
4877
5188
|
}
|
|
4878
5189
|
});
|
|
4879
5190
|
|
|
4880
|
-
// src/commands/
|
|
4881
|
-
var import_node_fs5 = require("fs");
|
|
4882
|
-
var import_node_path8 = require("path");
|
|
4883
|
-
var import_core23 = require("@penvhq/core");
|
|
5191
|
+
// src/commands/snapshot.ts
|
|
4884
5192
|
var import_citty17 = require("citty");
|
|
5193
|
+
function runSnapshot(options) {
|
|
5194
|
+
const project = openProject(options.cwd);
|
|
5195
|
+
return { write: writeSnapshotFile(project), wire: wireEnvModule(project) };
|
|
5196
|
+
}
|
|
5197
|
+
function renderSnapshot(result2) {
|
|
5198
|
+
const label = result2.write.action === "created" ? "Generated" : result2.write.action === "updated" ? "Updated" : "Up to date";
|
|
5199
|
+
const rows = [{ glyph: CHECK, label, subject: SNAPSHOT_FILE }];
|
|
5200
|
+
if (result2.wire.action === "wired") {
|
|
5201
|
+
rows.push({
|
|
5202
|
+
glyph: CHECK,
|
|
5203
|
+
label: "Wired",
|
|
5204
|
+
subject: result2.wire.file,
|
|
5205
|
+
detail: "load reads the snapshot"
|
|
5206
|
+
});
|
|
5207
|
+
} else if (result2.wire.action === "kept") {
|
|
5208
|
+
rows.push({ glyph: CHECK, label: "Wired", subject: result2.wire.file });
|
|
5209
|
+
}
|
|
5210
|
+
const lines = formatRows(rows);
|
|
5211
|
+
if (result2.wire.action === "manual") {
|
|
5212
|
+
lines.push(
|
|
5213
|
+
"",
|
|
5214
|
+
`${WARN} penv could not wire ${result2.wire.file} automatically \u2014 add these two lines yourself:`,
|
|
5215
|
+
tip(result2.wire.importLine),
|
|
5216
|
+
tip(result2.wire.loadHint)
|
|
5217
|
+
);
|
|
5218
|
+
}
|
|
5219
|
+
return lines;
|
|
5220
|
+
}
|
|
5221
|
+
var snapshotCommand = (0, import_citty17.defineCommand)({
|
|
5222
|
+
meta: {
|
|
5223
|
+
name: "snapshot",
|
|
5224
|
+
description: "Generate penv.snapshot.ts so load() resolves in a bundled/serverless runtime"
|
|
5225
|
+
},
|
|
5226
|
+
run() {
|
|
5227
|
+
return guard(async () => {
|
|
5228
|
+
write(renderSnapshot(runSnapshot({ cwd: process.cwd() })));
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
});
|
|
5232
|
+
|
|
5233
|
+
// src/commands/watch.ts
|
|
5234
|
+
var import_node_fs8 = require("fs");
|
|
5235
|
+
var import_node_path10 = require("path");
|
|
5236
|
+
var import_core24 = require("@penvhq/core");
|
|
5237
|
+
var import_citty18 = require("citty");
|
|
4885
5238
|
var DEBOUNCE_MS = 100;
|
|
4886
5239
|
function runWatch(options) {
|
|
4887
5240
|
const project = openProject(options.cwd);
|
|
4888
|
-
const configFile = (0,
|
|
5241
|
+
const configFile = (0, import_node_path10.basename)(project.configFile);
|
|
4889
5242
|
const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
|
|
4890
5243
|
const watchers = /* @__PURE__ */ new Set();
|
|
4891
5244
|
let timer;
|
|
@@ -4938,22 +5291,22 @@ function runWatch(options) {
|
|
|
4938
5291
|
watcher.close();
|
|
4939
5292
|
}
|
|
4940
5293
|
function armRecovery(target, recursive, only) {
|
|
4941
|
-
const parent = (0,
|
|
4942
|
-
const name = (0,
|
|
5294
|
+
const parent = (0, import_node_path10.dirname)(target);
|
|
5295
|
+
const name = (0, import_node_path10.basename)(target);
|
|
4943
5296
|
let recovery;
|
|
4944
5297
|
try {
|
|
4945
|
-
recovery = (0,
|
|
5298
|
+
recovery = (0, import_node_fs8.watch)(parent, { recursive: false }, (_event, filename) => {
|
|
4946
5299
|
if (closed || recovery === void 0) {
|
|
4947
5300
|
return;
|
|
4948
5301
|
}
|
|
4949
|
-
if (!(0,
|
|
5302
|
+
if (!(0, import_node_fs8.existsSync)(parent)) {
|
|
4950
5303
|
stop(recovery);
|
|
4951
5304
|
return;
|
|
4952
5305
|
}
|
|
4953
|
-
if (filename !== null && (0,
|
|
5306
|
+
if (filename !== null && (0, import_node_path10.basename)(filename) !== name) {
|
|
4954
5307
|
return;
|
|
4955
5308
|
}
|
|
4956
|
-
if (!(0,
|
|
5309
|
+
if (!(0, import_node_fs8.existsSync)(target)) {
|
|
4957
5310
|
return;
|
|
4958
5311
|
}
|
|
4959
5312
|
stop(recovery);
|
|
@@ -4973,11 +5326,11 @@ function runWatch(options) {
|
|
|
4973
5326
|
}
|
|
4974
5327
|
function addWatcher(target, recursive, only) {
|
|
4975
5328
|
let watcher;
|
|
4976
|
-
const listen = (useRecursive) => (0,
|
|
5329
|
+
const listen = (useRecursive) => (0, import_node_fs8.watch)(target, { recursive: useRecursive }, (_event, filename) => {
|
|
4977
5330
|
if (closed) {
|
|
4978
5331
|
return;
|
|
4979
5332
|
}
|
|
4980
|
-
if (!(0,
|
|
5333
|
+
if (!(0, import_node_fs8.existsSync)(target)) {
|
|
4981
5334
|
if (watcher !== void 0) {
|
|
4982
5335
|
stop(watcher);
|
|
4983
5336
|
}
|
|
@@ -4985,7 +5338,7 @@ function runWatch(options) {
|
|
|
4985
5338
|
schedule();
|
|
4986
5339
|
return;
|
|
4987
5340
|
}
|
|
4988
|
-
if (only !== void 0 && (filename === null || (0,
|
|
5341
|
+
if (only !== void 0 && (filename === null || (0, import_node_path10.basename)(filename) !== only)) {
|
|
4989
5342
|
return;
|
|
4990
5343
|
}
|
|
4991
5344
|
schedule();
|
|
@@ -5012,10 +5365,11 @@ function runWatch(options) {
|
|
|
5012
5365
|
watchers.add(watcher);
|
|
5013
5366
|
}
|
|
5014
5367
|
addWatcher(project.penvDir, true);
|
|
5015
|
-
addWatcher((0,
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5368
|
+
addWatcher((0, import_node_path10.dirname)(project.configFile), false, configFile);
|
|
5369
|
+
addWatcher(project.root, false, import_core24.SCHEMA_SHAPE_FILE);
|
|
5370
|
+
if ((0, import_core24.schemaInsideTree)(project.config) === void 0) {
|
|
5371
|
+
const schemaFile = (0, import_node_path10.resolve)(project.root, (0, import_core24.schemaFileOf)(project.config));
|
|
5372
|
+
addWatcher((0, import_node_path10.dirname)(schemaFile), false, (0, import_node_path10.basename)(schemaFile));
|
|
5019
5373
|
}
|
|
5020
5374
|
void validate();
|
|
5021
5375
|
return {
|
|
@@ -5058,7 +5412,7 @@ function renderDrift(drift, environment) {
|
|
|
5058
5412
|
}
|
|
5059
5413
|
return lines;
|
|
5060
5414
|
}
|
|
5061
|
-
var watchCommand = (0,
|
|
5415
|
+
var watchCommand = (0, import_citty18.defineCommand)({
|
|
5062
5416
|
meta: {
|
|
5063
5417
|
name: "watch",
|
|
5064
5418
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -5097,7 +5451,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
|
|
|
5097
5451
|
});
|
|
5098
5452
|
|
|
5099
5453
|
// src/index.ts
|
|
5100
|
-
var main = (0,
|
|
5454
|
+
var main = (0, import_citty19.defineCommand)({
|
|
5101
5455
|
meta: {
|
|
5102
5456
|
name: "penv",
|
|
5103
5457
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -5118,14 +5472,15 @@ var main = (0, import_citty18.defineCommand)({
|
|
|
5118
5472
|
encrypt: encryptCommand,
|
|
5119
5473
|
decrypt: decryptCommand,
|
|
5120
5474
|
key: keyCommand,
|
|
5475
|
+
snapshot: snapshotCommand,
|
|
5121
5476
|
validate: validateCommand,
|
|
5122
5477
|
doctor: doctorCommand,
|
|
5123
5478
|
watch: watchCommand
|
|
5124
5479
|
}
|
|
5125
5480
|
});
|
|
5126
5481
|
function runMain() {
|
|
5127
|
-
(0,
|
|
5128
|
-
return (0,
|
|
5482
|
+
(0, import_core25.setKeychain)(defaultKeychain);
|
|
5483
|
+
return (0, import_citty19.runMain)(main);
|
|
5129
5484
|
}
|
|
5130
5485
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5131
5486
|
0 && (module.exports = {
|
|
@@ -5140,6 +5495,7 @@ function runMain() {
|
|
|
5140
5495
|
renderPull,
|
|
5141
5496
|
renderPush,
|
|
5142
5497
|
renderRotate,
|
|
5498
|
+
renderSnapshot,
|
|
5143
5499
|
renderWatch,
|
|
5144
5500
|
runDecrypt,
|
|
5145
5501
|
runDoctor,
|
|
@@ -5157,6 +5513,7 @@ function runMain() {
|
|
|
5157
5513
|
runRemove,
|
|
5158
5514
|
runRotate,
|
|
5159
5515
|
runSet,
|
|
5516
|
+
runSnapshot,
|
|
5160
5517
|
runValidate,
|
|
5161
5518
|
runWatch
|
|
5162
5519
|
});
|