@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.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { setKeychain } from "@penvhq/core";
|
|
3
|
-
import { runMain as cittyRunMain, defineCommand as
|
|
3
|
+
import { runMain as cittyRunMain, defineCommand as defineCommand19 } from "citty";
|
|
4
4
|
|
|
5
5
|
// src/commands/doctor.ts
|
|
6
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
7
|
+
import { join as join3 } from "path";
|
|
6
8
|
import {
|
|
7
9
|
accessPath as accessPath3,
|
|
8
10
|
assertNever,
|
|
9
11
|
effectiveMeta,
|
|
10
|
-
formatValueFile as
|
|
12
|
+
formatValueFile as formatValueFile3,
|
|
11
13
|
holdsProjection as holdsProjection3,
|
|
12
14
|
holdsRecords,
|
|
13
15
|
isPublicVariable,
|
|
@@ -81,7 +83,8 @@ function environmentFromShorthand(config, candidates, explicit) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// src/project.ts
|
|
84
|
-
import {
|
|
86
|
+
import { existsSync, readFileSync } from "fs";
|
|
87
|
+
import { dirname as dirname2, join, resolve as resolve2 } from "path";
|
|
85
88
|
import {
|
|
86
89
|
candidatesFor,
|
|
87
90
|
formatValueFile,
|
|
@@ -94,7 +97,9 @@ import {
|
|
|
94
97
|
ReservedTokenError,
|
|
95
98
|
refFromAccessPath,
|
|
96
99
|
resolveEnvironment,
|
|
97
|
-
resolveKeySource
|
|
100
|
+
resolveKeySource,
|
|
101
|
+
SCHEMA_SHAPE_FILE,
|
|
102
|
+
schemaFileOf
|
|
98
103
|
} from "@penvhq/core";
|
|
99
104
|
import { FilesystemProvider } from "@penvhq/provider-filesystem";
|
|
100
105
|
|
|
@@ -248,6 +253,18 @@ function localTree(project) {
|
|
|
248
253
|
}
|
|
249
254
|
return project.provider;
|
|
250
255
|
}
|
|
256
|
+
function selfContainedSchemaModule(root, schemaFile) {
|
|
257
|
+
const file = join(root, ...schemaFile.split("/"));
|
|
258
|
+
if (!existsSync(file)) {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
const source = readFileSync(file, "utf8");
|
|
262
|
+
return source.includes("PenvSchemaShape") || source.includes("z.object") ? schemaFile : void 0;
|
|
263
|
+
}
|
|
264
|
+
function schemaShapeFileOf(project) {
|
|
265
|
+
const schemaFile = schemaFileOf(project.config);
|
|
266
|
+
return selfContainedSchemaModule(project.root, schemaFile) ?? SCHEMA_SHAPE_FILE;
|
|
267
|
+
}
|
|
251
268
|
async function sourceProviderFor(project, environment) {
|
|
252
269
|
const providerConfig = project.config.providers[environment];
|
|
253
270
|
if (providerConfig === void 0) {
|
|
@@ -479,7 +496,7 @@ function hasValue(resolution) {
|
|
|
479
496
|
return resolution.winner !== void 0;
|
|
480
497
|
}
|
|
481
498
|
function computeDrift(input) {
|
|
482
|
-
const { schema, resolutions, config, environment } = input;
|
|
499
|
+
const { schema, resolutions, config, environment, schemaShapeFile } = input;
|
|
483
500
|
const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
|
|
484
501
|
const declared = [];
|
|
485
502
|
const optional = [];
|
|
@@ -489,7 +506,7 @@ function computeDrift(input) {
|
|
|
489
506
|
}
|
|
490
507
|
const path = leaf.path.join(".");
|
|
491
508
|
const ref = refFromAccessPath2(leaf.path);
|
|
492
|
-
const renameRemedy = `Rename the \`${path}\` key in
|
|
509
|
+
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.`;
|
|
493
510
|
if (ref === void 0 || isReservedToken2(ref.name, config)) {
|
|
494
511
|
if (leaf.absencePermitted) {
|
|
495
512
|
optional.push({ subject: path, remedy: renameRemedy });
|
|
@@ -519,7 +536,7 @@ function computeDrift(input) {
|
|
|
519
536
|
subject: parameterId2(ref),
|
|
520
537
|
ref,
|
|
521
538
|
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`,
|
|
522
|
-
detail: `declared in
|
|
539
|
+
detail: `declared in ${schemaShapeFile}, no value for ${environment}`
|
|
523
540
|
});
|
|
524
541
|
}
|
|
525
542
|
const undeclared = [];
|
|
@@ -535,6 +552,133 @@ function computeDrift(input) {
|
|
|
535
552
|
return { declared, undeclared, optional };
|
|
536
553
|
}
|
|
537
554
|
|
|
555
|
+
// src/snapshot.ts
|
|
556
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
557
|
+
import { join as join2 } from "path";
|
|
558
|
+
import { formatValueFile as formatValueFile2, schemaFileOf as schemaFileOf2 } from "@penvhq/core";
|
|
559
|
+
import { createFilesystemProvider as createFilesystemProvider2 } from "@penvhq/provider-filesystem";
|
|
560
|
+
var SNAPSHOT_FILE = "penv.snapshot.ts";
|
|
561
|
+
function sealedValues(tree) {
|
|
562
|
+
const collected = {};
|
|
563
|
+
for (const file of tree.listSync()) {
|
|
564
|
+
if (!file.encrypted) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (file.scope.kind === "local" || file.scope.kind === "environment-local") {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const stored = tree.readSync(file);
|
|
571
|
+
if (stored === void 0) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
collected[formatValueFile2(file)] = stored;
|
|
575
|
+
}
|
|
576
|
+
const values = {};
|
|
577
|
+
for (const key of Object.keys(collected).sort()) {
|
|
578
|
+
values[key] = collected[key];
|
|
579
|
+
}
|
|
580
|
+
return values;
|
|
581
|
+
}
|
|
582
|
+
function buildSnapshotFrom(root, config) {
|
|
583
|
+
const tree = createFilesystemProvider2({ root: join2(root, PENV_DIR), config });
|
|
584
|
+
return { v: 1, config, values: sealedValues(tree) };
|
|
585
|
+
}
|
|
586
|
+
function buildSnapshot(project) {
|
|
587
|
+
return { v: 1, config: project.config, values: sealedValues(localTree(project)) };
|
|
588
|
+
}
|
|
589
|
+
function renderSnapshotModule(snapshot) {
|
|
590
|
+
const body = JSON.stringify(snapshot, null, 2);
|
|
591
|
+
return `// penv.snapshot.ts \u2014 GENERATED by penv. Do not edit; run \`penv snapshot\` to refresh.
|
|
592
|
+
// It embeds the evaluated config and every committed sealed (.enc) value, so \`load()\`
|
|
593
|
+
// resolves in a bundled or serverless runtime where no penv.config.ts is on disk.
|
|
594
|
+
import type { PenvSnapshot } from "@penvhq/penv";
|
|
595
|
+
|
|
596
|
+
export const snapshot = ${body} satisfies PenvSnapshot;
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
function equalsIgnoringEol(a, b) {
|
|
600
|
+
return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
|
|
601
|
+
}
|
|
602
|
+
function writeSnapshotFile(project) {
|
|
603
|
+
const path = join2(project.root, SNAPSHOT_FILE);
|
|
604
|
+
const wanted = renderSnapshotModule(buildSnapshot(project));
|
|
605
|
+
const existing = existsSync2(path) ? readFileSync2(path, "utf8") : void 0;
|
|
606
|
+
if (existing !== void 0 && equalsIgnoringEol(existing, wanted)) {
|
|
607
|
+
return { file: SNAPSHOT_FILE, action: "unchanged" };
|
|
608
|
+
}
|
|
609
|
+
writeFileSync(path, wanted, "utf8");
|
|
610
|
+
return { file: SNAPSHOT_FILE, action: existing === void 0 ? "created" : "updated" };
|
|
611
|
+
}
|
|
612
|
+
function snapshotExists(root) {
|
|
613
|
+
return existsSync2(join2(root, SNAPSHOT_FILE));
|
|
614
|
+
}
|
|
615
|
+
function refreshSnapshot(project) {
|
|
616
|
+
if (snapshotExists(project.root)) {
|
|
617
|
+
writeSnapshotFile(project);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function snapshotSpecifier(schemaFile) {
|
|
621
|
+
const depth = schemaFile.split("/").length - 1;
|
|
622
|
+
const climb = depth === 0 ? "./" : "../".repeat(depth);
|
|
623
|
+
return `${climb}penv.snapshot.js`;
|
|
624
|
+
}
|
|
625
|
+
function insertSnapshotImport(source, importLine) {
|
|
626
|
+
const afterSchema = /^import\s*\{\s*schema\s*\}\s*from\s*["'][^"']*penv\.schema\.js["'];?[ \t]*\r?\n/m;
|
|
627
|
+
const schemaMatch = afterSchema.exec(source);
|
|
628
|
+
if (schemaMatch !== null) {
|
|
629
|
+
const index = schemaMatch.index + schemaMatch[0].length;
|
|
630
|
+
return `${source.slice(0, index)}${importLine}
|
|
631
|
+
${source.slice(index)}`;
|
|
632
|
+
}
|
|
633
|
+
const afterLoad = /^import\s*\{[^}]*\}\s*from\s*["']@penvhq\/penv["'];?[ \t]*\r?\n/m;
|
|
634
|
+
const loadMatch = afterLoad.exec(source);
|
|
635
|
+
if (loadMatch !== null) {
|
|
636
|
+
const index = loadMatch.index + loadMatch[0].length;
|
|
637
|
+
return `${source.slice(0, index)}${importLine}
|
|
638
|
+
${source.slice(index)}`;
|
|
639
|
+
}
|
|
640
|
+
return `${importLine}
|
|
641
|
+
${source}`;
|
|
642
|
+
}
|
|
643
|
+
function wireEnvModule(project) {
|
|
644
|
+
const schemaFile = schemaFileOf2(project.config);
|
|
645
|
+
const specifier = snapshotSpecifier(schemaFile);
|
|
646
|
+
const importLine = `import { snapshot } from "${specifier}";`;
|
|
647
|
+
const loadHint = "pass it to load: `load(schema, { snapshot })`";
|
|
648
|
+
const manual = { file: schemaFile, action: "manual", importLine, loadHint };
|
|
649
|
+
const path = join2(project.root, ...schemaFile.split("/"));
|
|
650
|
+
if (!existsSync2(path)) {
|
|
651
|
+
return manual;
|
|
652
|
+
}
|
|
653
|
+
const source = readFileSync2(path, "utf8");
|
|
654
|
+
const loadCall = /load\(\s*schema\s*(\)|,\s*\{([\s\S]*?)\}\s*\))/;
|
|
655
|
+
const match = loadCall.exec(source);
|
|
656
|
+
if (match === null) {
|
|
657
|
+
return manual;
|
|
658
|
+
}
|
|
659
|
+
const options = match[2];
|
|
660
|
+
const snapshotProperty = /(?:^|[,{\s])snapshot\s*(?=[,:}]|$)/;
|
|
661
|
+
const snapshotInOptions = options !== void 0 && snapshotProperty.test(options);
|
|
662
|
+
const importPresent = source.includes(importLine);
|
|
663
|
+
if (snapshotInOptions && importPresent) {
|
|
664
|
+
return { file: schemaFile, action: "kept", importLine, loadHint };
|
|
665
|
+
}
|
|
666
|
+
let updated = source;
|
|
667
|
+
if (!snapshotInOptions) {
|
|
668
|
+
const trimmed = (options ?? "").trim();
|
|
669
|
+
const replacement = match[1] === ")" || trimmed.length === 0 ? "load(schema, { snapshot })" : `load(schema, { ${trimmed}, snapshot })`;
|
|
670
|
+
updated = updated.replace(loadCall, replacement);
|
|
671
|
+
}
|
|
672
|
+
if (!importPresent) {
|
|
673
|
+
updated = insertSnapshotImport(updated, importLine);
|
|
674
|
+
}
|
|
675
|
+
if (updated === source) {
|
|
676
|
+
return { file: schemaFile, action: "kept", importLine, loadHint };
|
|
677
|
+
}
|
|
678
|
+
writeFileSync(path, updated, "utf8");
|
|
679
|
+
return { file: schemaFile, action: "wired", importLine, loadHint };
|
|
680
|
+
}
|
|
681
|
+
|
|
538
682
|
// src/style.ts
|
|
539
683
|
function supportsColor(stream) {
|
|
540
684
|
const env = process.env;
|
|
@@ -1077,7 +1221,7 @@ import {
|
|
|
1077
1221
|
ReservedTokenError as ReservedTokenError2,
|
|
1078
1222
|
resolveAll,
|
|
1079
1223
|
SCHEMA_HARVEST_ENV,
|
|
1080
|
-
schemaFileOf,
|
|
1224
|
+
schemaFileOf as schemaFileOf3,
|
|
1081
1225
|
validateConfig
|
|
1082
1226
|
} from "@penvhq/core";
|
|
1083
1227
|
import { defineCommand as defineCommand2 } from "citty";
|
|
@@ -1126,7 +1270,7 @@ function place(root, path, value) {
|
|
|
1126
1270
|
function isZodType(value) {
|
|
1127
1271
|
return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
|
|
1128
1272
|
}
|
|
1129
|
-
function validationIssuesOf(cause, schemaPath) {
|
|
1273
|
+
function validationIssuesOf(cause, schemaPath, schemaShapeFile) {
|
|
1130
1274
|
if (typeof cause !== "object" || cause === null) {
|
|
1131
1275
|
return void 0;
|
|
1132
1276
|
}
|
|
@@ -1140,7 +1284,10 @@ function validationIssuesOf(cause, schemaPath) {
|
|
|
1140
1284
|
kind: "schema",
|
|
1141
1285
|
subject: typeof parameter === "string" && parameter !== "" ? parameter : schemaPath,
|
|
1142
1286
|
message: typeof message === "string" ? message : String(issue),
|
|
1143
|
-
|
|
1287
|
+
// `schemaPath` is the loader penv evaluated; the shape to adjust lives in
|
|
1288
|
+
// `schemaShapeFile`, which is that same file for an old-layout project and
|
|
1289
|
+
// `penv.schema.ts` for one on the split.
|
|
1290
|
+
remedy: `Fix the value, or adjust the schema in ${schemaShapeFile} if the shape is wrong.`
|
|
1144
1291
|
};
|
|
1145
1292
|
});
|
|
1146
1293
|
}
|
|
@@ -1154,11 +1301,12 @@ function exclusively(work) {
|
|
|
1154
1301
|
return result2;
|
|
1155
1302
|
}
|
|
1156
1303
|
function loadSchema(project, environment) {
|
|
1157
|
-
const schemaPath =
|
|
1304
|
+
const schemaPath = schemaFileOf3(project.config);
|
|
1305
|
+
const schemaShapeFile = schemaShapeFileOf(project);
|
|
1158
1306
|
const file = pathToFileURL2(resolvePath(project.root, schemaPath)).href;
|
|
1159
|
-
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
1307
|
+
return exclusively(() => loadSchemaExclusively(file, schemaPath, schemaShapeFile, environment));
|
|
1160
1308
|
}
|
|
1161
|
-
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
1309
|
+
async function loadSchemaExclusively(file, schemaPath, schemaShapeFile, environment) {
|
|
1162
1310
|
const jiti = jitiFor(file);
|
|
1163
1311
|
const previous = process.env.PENV_ENV;
|
|
1164
1312
|
const previousHarvest = process.env[SCHEMA_HARVEST_ENV];
|
|
@@ -1168,7 +1316,7 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
1168
1316
|
try {
|
|
1169
1317
|
loaded = await jiti.import(file);
|
|
1170
1318
|
} catch (cause) {
|
|
1171
|
-
const issues = validationIssuesOf(cause, schemaPath);
|
|
1319
|
+
const issues = validationIssuesOf(cause, schemaPath, schemaShapeFile);
|
|
1172
1320
|
if (issues !== void 0) {
|
|
1173
1321
|
return { issues };
|
|
1174
1322
|
}
|
|
@@ -1213,7 +1361,8 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
1213
1361
|
async function runValidate(options) {
|
|
1214
1362
|
const project = openProject(options.cwd);
|
|
1215
1363
|
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
1216
|
-
const schemaPath =
|
|
1364
|
+
const schemaPath = schemaFileOf3(project.config);
|
|
1365
|
+
const schemaShapeFile = schemaShapeFileOf(project);
|
|
1217
1366
|
const issues = [];
|
|
1218
1367
|
let files;
|
|
1219
1368
|
try {
|
|
@@ -1263,7 +1412,13 @@ async function runValidate(options) {
|
|
|
1263
1412
|
place(object, accessPath2(resolution.ref), resolution.value);
|
|
1264
1413
|
}
|
|
1265
1414
|
}
|
|
1266
|
-
drift = computeDrift({
|
|
1415
|
+
drift = computeDrift({
|
|
1416
|
+
schema,
|
|
1417
|
+
resolutions,
|
|
1418
|
+
config: project.config,
|
|
1419
|
+
environment,
|
|
1420
|
+
schemaShapeFile
|
|
1421
|
+
});
|
|
1267
1422
|
const result2 = schema.safeParse(object);
|
|
1268
1423
|
if (!result2.success) {
|
|
1269
1424
|
for (const problem of result2.error.issues) {
|
|
@@ -1275,7 +1430,7 @@ async function runValidate(options) {
|
|
|
1275
1430
|
kind: "schema",
|
|
1276
1431
|
subject: path || schemaPath,
|
|
1277
1432
|
message: problem.message,
|
|
1278
|
-
remedy: `Fix the value, or adjust the schema in ${
|
|
1433
|
+
remedy: `Fix the value, or adjust the schema in ${schemaShapeFile} if the shape is wrong.`
|
|
1279
1434
|
});
|
|
1280
1435
|
}
|
|
1281
1436
|
}
|
|
@@ -1398,13 +1553,15 @@ async function runDoctor(options) {
|
|
|
1398
1553
|
skipped("unused", "Value coverage")
|
|
1399
1554
|
);
|
|
1400
1555
|
} else {
|
|
1556
|
+
const schemaShapeFile = schemaShapeFileOf(project);
|
|
1401
1557
|
const drift = computeDrift({
|
|
1402
1558
|
schema,
|
|
1403
1559
|
resolutions: subjects.map(({ resolution }) => resolution),
|
|
1404
1560
|
config: project.config,
|
|
1405
|
-
environment
|
|
1561
|
+
environment,
|
|
1562
|
+
schemaShapeFile
|
|
1406
1563
|
});
|
|
1407
|
-
findings.push(...declaredFindings(drift, missing, environment));
|
|
1564
|
+
findings.push(...declaredFindings(drift, missing, environment, schemaShapeFile));
|
|
1408
1565
|
findings.push(...weakFindings(subjects, schema));
|
|
1409
1566
|
findings.push(...unusedFindings(drift));
|
|
1410
1567
|
}
|
|
@@ -1420,6 +1577,8 @@ async function runDoctor(options) {
|
|
|
1420
1577
|
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1421
1578
|
}
|
|
1422
1579
|
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
1580
|
+
findings.push(...snapshotStaleFindings(project));
|
|
1581
|
+
findings.push(...bundleInvisibleFindings(project, subjects, environment));
|
|
1423
1582
|
findings.push(...await projectionFindings(project, environment, options.projection));
|
|
1424
1583
|
findings.push({
|
|
1425
1584
|
check: "provider",
|
|
@@ -1455,7 +1614,7 @@ function missingFindings(subjects, environment) {
|
|
|
1455
1614
|
}
|
|
1456
1615
|
];
|
|
1457
1616
|
}
|
|
1458
|
-
function declaredFindings(drift, missing, environment) {
|
|
1617
|
+
function declaredFindings(drift, missing, environment, schemaShapeFile) {
|
|
1459
1618
|
const reported = new Set(missing.filter((f) => f.severity !== "pass").map((f) => f.subject));
|
|
1460
1619
|
const findings = drift.declared.filter((item) => !reported.has(item.subject)).map((item) => ({
|
|
1461
1620
|
check: "declared",
|
|
@@ -1473,7 +1632,7 @@ function declaredFindings(drift, missing, environment) {
|
|
|
1473
1632
|
check: "declared",
|
|
1474
1633
|
severity: "pass",
|
|
1475
1634
|
label: "Schema coverage",
|
|
1476
|
-
subject: `every parameter
|
|
1635
|
+
subject: `every parameter ${schemaShapeFile} requires has a value for ${environment}`
|
|
1477
1636
|
}
|
|
1478
1637
|
];
|
|
1479
1638
|
}
|
|
@@ -2045,7 +2204,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
2045
2204
|
check: "provider-value-drift",
|
|
2046
2205
|
severity: "unknown",
|
|
2047
2206
|
label: "Provider value unreadable",
|
|
2048
|
-
subject:
|
|
2207
|
+
subject: formatValueFile3(here.file),
|
|
2049
2208
|
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
2050
2209
|
});
|
|
2051
2210
|
continue;
|
|
@@ -2055,7 +2214,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
2055
2214
|
check: "provider-value-drift",
|
|
2056
2215
|
severity: "failure",
|
|
2057
2216
|
label: "Provider value drift",
|
|
2058
|
-
subject:
|
|
2217
|
+
subject: formatValueFile3(here.file),
|
|
2059
2218
|
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
2060
2219
|
});
|
|
2061
2220
|
}
|
|
@@ -2069,7 +2228,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
2069
2228
|
check: "provider-value-drift",
|
|
2070
2229
|
severity: "warning",
|
|
2071
2230
|
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
2072
|
-
subject:
|
|
2231
|
+
subject: formatValueFile3(present.file),
|
|
2073
2232
|
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`
|
|
2074
2233
|
});
|
|
2075
2234
|
}
|
|
@@ -2085,6 +2244,68 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
2085
2244
|
}
|
|
2086
2245
|
];
|
|
2087
2246
|
}
|
|
2247
|
+
function snapshotStaleFindings(project) {
|
|
2248
|
+
if (!snapshotExists(project.root)) {
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
const wanted = renderSnapshotModule(buildSnapshot(project));
|
|
2252
|
+
const committed = readFileSync3(join3(project.root, SNAPSHOT_FILE), "utf8");
|
|
2253
|
+
if (equalsIgnoringEol(committed, wanted)) {
|
|
2254
|
+
return [
|
|
2255
|
+
{
|
|
2256
|
+
check: "snapshot-stale",
|
|
2257
|
+
severity: "pass",
|
|
2258
|
+
label: "Snapshot",
|
|
2259
|
+
subject: `${SNAPSHOT_FILE} matches the committed sealed values and config`
|
|
2260
|
+
}
|
|
2261
|
+
];
|
|
2262
|
+
}
|
|
2263
|
+
return [
|
|
2264
|
+
{
|
|
2265
|
+
check: "snapshot-stale",
|
|
2266
|
+
severity: "failure",
|
|
2267
|
+
label: "Snapshot stale",
|
|
2268
|
+
subject: SNAPSHOT_FILE,
|
|
2269
|
+
detail: "does not match the current sealed values or config, so a bundle resolves stale data",
|
|
2270
|
+
remedy: "penv snapshot"
|
|
2271
|
+
}
|
|
2272
|
+
];
|
|
2273
|
+
}
|
|
2274
|
+
function bundleInvisibleFindings(project, subjects, environment) {
|
|
2275
|
+
if (!snapshotExists(project.root)) {
|
|
2276
|
+
return [];
|
|
2277
|
+
}
|
|
2278
|
+
const findings = [];
|
|
2279
|
+
for (const { resolution } of subjects) {
|
|
2280
|
+
const winner = resolution.winner;
|
|
2281
|
+
if (winner === void 0 || winner.file.encrypted) {
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
const scope = winner.file.scope;
|
|
2285
|
+
if (scope.kind === "local" || scope.kind === "environment-local") {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
findings.push({
|
|
2289
|
+
check: "bundle-invisible-plaintext",
|
|
2290
|
+
severity: "warning",
|
|
2291
|
+
label: "Invisible to bundles",
|
|
2292
|
+
subject: winner.location,
|
|
2293
|
+
detail: "plaintext is gitignored and not embedded in the snapshot, so a bundle cannot see it",
|
|
2294
|
+
remedy: `penv encrypt ${refPathOf(resolution.ref)} --env ${environment}`
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
if (findings.length > 0) {
|
|
2298
|
+
return findings;
|
|
2299
|
+
}
|
|
2300
|
+
return [
|
|
2301
|
+
{
|
|
2302
|
+
check: "bundle-invisible-plaintext",
|
|
2303
|
+
severity: "pass",
|
|
2304
|
+
label: "Bundle coverage",
|
|
2305
|
+
subject: `every value resolving for ${environment} is embeddable in ${SNAPSHOT_FILE}`
|
|
2306
|
+
}
|
|
2307
|
+
];
|
|
2308
|
+
}
|
|
2088
2309
|
var GLYPHS = {
|
|
2089
2310
|
pass: CHECK,
|
|
2090
2311
|
warning: WARN,
|
|
@@ -2157,7 +2378,7 @@ var doctorCommand = defineCommand3({
|
|
|
2157
2378
|
|
|
2158
2379
|
// src/commands/encrypt.ts
|
|
2159
2380
|
import {
|
|
2160
|
-
formatValueFile as
|
|
2381
|
+
formatValueFile as formatValueFile5,
|
|
2161
2382
|
isSecret as isSecret3,
|
|
2162
2383
|
openValue as openValue3,
|
|
2163
2384
|
PenvError as PenvError8,
|
|
@@ -2167,7 +2388,7 @@ import {
|
|
|
2167
2388
|
import { defineCommand as defineCommand5 } from "citty";
|
|
2168
2389
|
|
|
2169
2390
|
// src/commands/set.ts
|
|
2170
|
-
import { formatValueFile as
|
|
2391
|
+
import { formatValueFile as formatValueFile4, isSecret as isSecret2, PenvError as PenvError7, parameterId as parameterId3, sealValue } from "@penvhq/core";
|
|
2171
2392
|
import { defineCommand as defineCommand4 } from "citty";
|
|
2172
2393
|
function scopeFrom(options) {
|
|
2173
2394
|
if (options.local === true) {
|
|
@@ -2202,7 +2423,7 @@ function sealFor(project, file, value, parameter, environment) {
|
|
|
2202
2423
|
if (environment === void 0) {
|
|
2203
2424
|
throw new PenvError7(
|
|
2204
2425
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
2205
|
-
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${
|
|
2426
|
+
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile4(file)} names no environment`,
|
|
2206
2427
|
"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."
|
|
2207
2428
|
);
|
|
2208
2429
|
}
|
|
@@ -2220,7 +2441,7 @@ async function sealAwareWrite(options) {
|
|
|
2220
2441
|
const stored = secret ? sealFor(project, file, value, parameterId3(ref), environment) : value;
|
|
2221
2442
|
await provider.write(file, stored);
|
|
2222
2443
|
await provider.remove({ ...file, encrypted: !secret });
|
|
2223
|
-
return { encrypted: secret, location:
|
|
2444
|
+
return { encrypted: secret, location: formatValueFile4(file) };
|
|
2224
2445
|
}
|
|
2225
2446
|
async function runSet(options) {
|
|
2226
2447
|
const project = openProject(options.cwd);
|
|
@@ -2236,6 +2457,7 @@ async function runSet(options) {
|
|
|
2236
2457
|
value: options.value,
|
|
2237
2458
|
environment
|
|
2238
2459
|
});
|
|
2460
|
+
refreshSnapshot(project);
|
|
2239
2461
|
return { parameter: options.key, location, encrypted };
|
|
2240
2462
|
}
|
|
2241
2463
|
function renderSet(result2) {
|
|
@@ -2323,17 +2545,18 @@ async function runEncrypt(options) {
|
|
|
2323
2545
|
const already = await readOne(project, sealed);
|
|
2324
2546
|
throw new PenvError8(
|
|
2325
2547
|
"PARAMETER_ABSENT",
|
|
2326
|
-
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${
|
|
2548
|
+
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile5(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile5(sealed)}`,
|
|
2327
2549
|
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."
|
|
2328
2550
|
);
|
|
2329
2551
|
}
|
|
2330
2552
|
const text = sealValue2(sealed, value, keySourceFor(project, environment), parameter, environment);
|
|
2331
2553
|
await project.provider.write(sealed, text);
|
|
2332
2554
|
await project.provider.remove(plain);
|
|
2555
|
+
refreshSnapshot(project);
|
|
2333
2556
|
return {
|
|
2334
2557
|
parameter,
|
|
2335
|
-
location:
|
|
2336
|
-
removed:
|
|
2558
|
+
location: formatValueFile5(sealed),
|
|
2559
|
+
removed: formatValueFile5(plain)
|
|
2337
2560
|
};
|
|
2338
2561
|
}
|
|
2339
2562
|
async function runDecrypt(options) {
|
|
@@ -2352,7 +2575,7 @@ async function runDecrypt(options) {
|
|
|
2352
2575
|
if (stored === void 0) {
|
|
2353
2576
|
throw new PenvError8(
|
|
2354
2577
|
"PARAMETER_ABSENT",
|
|
2355
|
-
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${
|
|
2578
|
+
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile5(sealed)}`,
|
|
2356
2579
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
2357
2580
|
);
|
|
2358
2581
|
}
|
|
@@ -2361,16 +2584,17 @@ async function runDecrypt(options) {
|
|
|
2361
2584
|
throw new UndecryptableAt(
|
|
2362
2585
|
parameter,
|
|
2363
2586
|
environment,
|
|
2364
|
-
|
|
2587
|
+
formatValueFile5(sealed),
|
|
2365
2588
|
opened.failure.detail
|
|
2366
2589
|
);
|
|
2367
2590
|
}
|
|
2368
2591
|
await project.provider.write(plain, opened.value);
|
|
2369
2592
|
await project.provider.remove(sealed);
|
|
2593
|
+
refreshSnapshot(project);
|
|
2370
2594
|
return {
|
|
2371
2595
|
parameter,
|
|
2372
|
-
location:
|
|
2373
|
-
removed:
|
|
2596
|
+
location: formatValueFile5(plain),
|
|
2597
|
+
removed: formatValueFile5(sealed)
|
|
2374
2598
|
};
|
|
2375
2599
|
}
|
|
2376
2600
|
var UndecryptableAt = class extends PenvError8 {
|
|
@@ -2578,7 +2802,7 @@ var fillCommand = defineCommand6({
|
|
|
2578
2802
|
});
|
|
2579
2803
|
|
|
2580
2804
|
// src/commands/generate.ts
|
|
2581
|
-
import { writeFileSync } from "fs";
|
|
2805
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2582
2806
|
import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
2583
2807
|
import {
|
|
2584
2808
|
checkNameCollisions as checkNameCollisions3,
|
|
@@ -2639,7 +2863,7 @@ function runGenerate(options) {
|
|
|
2639
2863
|
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2640
2864
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
2641
2865
|
const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
|
|
2642
|
-
|
|
2866
|
+
writeFileSync2(file, serializeDotenv(entries), "utf8");
|
|
2643
2867
|
return { file, environment, entries: entries.length, decrypted };
|
|
2644
2868
|
}
|
|
2645
2869
|
function displayPath(cwd, file) {
|
|
@@ -2777,7 +3001,7 @@ var getCommand = defineCommand8({
|
|
|
2777
3001
|
});
|
|
2778
3002
|
|
|
2779
3003
|
// src/commands/import.ts
|
|
2780
|
-
import { copyFileSync, existsSync as
|
|
3004
|
+
import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
2781
3005
|
import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
|
|
2782
3006
|
import {
|
|
2783
3007
|
accessPath as accessPath4,
|
|
@@ -2793,15 +3017,15 @@ import {
|
|
|
2793
3017
|
ReservedTokenError as ReservedTokenError3,
|
|
2794
3018
|
refFromVariable,
|
|
2795
3019
|
roundTripsCleanly,
|
|
2796
|
-
schemaFileOf as
|
|
3020
|
+
schemaFileOf as schemaFileOf5,
|
|
2797
3021
|
UnknownEnvironmentError,
|
|
2798
3022
|
variableName as variableName5
|
|
2799
3023
|
} from "@penvhq/core";
|
|
2800
3024
|
import { defineCommand as defineCommand10 } from "citty";
|
|
2801
3025
|
|
|
2802
3026
|
// src/detect.ts
|
|
2803
|
-
import { existsSync, readFileSync } from "fs";
|
|
2804
|
-
import { join } from "path";
|
|
3027
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
3028
|
+
import { join as join4 } from "path";
|
|
2805
3029
|
import { DEFAULT_SCHEMA_FILE } from "@penvhq/core";
|
|
2806
3030
|
var SIGNATURES = [
|
|
2807
3031
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
@@ -2818,7 +3042,7 @@ var SIGNATURES = [
|
|
|
2818
3042
|
function exportsSchema(file) {
|
|
2819
3043
|
let source;
|
|
2820
3044
|
try {
|
|
2821
|
-
source =
|
|
3045
|
+
source = readFileSync4(file, "utf8");
|
|
2822
3046
|
} catch {
|
|
2823
3047
|
return false;
|
|
2824
3048
|
}
|
|
@@ -2829,8 +3053,8 @@ function exportsSchema(file) {
|
|
|
2829
3053
|
);
|
|
2830
3054
|
}
|
|
2831
3055
|
function occupied(cwd, relative3) {
|
|
2832
|
-
const file =
|
|
2833
|
-
return
|
|
3056
|
+
const file = join4(cwd, ...relative3.split("/"));
|
|
3057
|
+
return existsSync3(file) && !exportsSchema(file);
|
|
2834
3058
|
}
|
|
2835
3059
|
function schemaFileFor(cwd) {
|
|
2836
3060
|
const dir = srcPrefix(cwd);
|
|
@@ -2845,7 +3069,7 @@ function schemaFileFor(cwd) {
|
|
|
2845
3069
|
return { file: DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2846
3070
|
}
|
|
2847
3071
|
function srcPrefix(cwd) {
|
|
2848
|
-
return
|
|
3072
|
+
return existsSync3(join4(cwd, "src")) ? "src/" : "";
|
|
2849
3073
|
}
|
|
2850
3074
|
function dependenciesOf(cwd) {
|
|
2851
3075
|
const manifest = manifestOf(cwd);
|
|
@@ -2864,13 +3088,13 @@ function dependenciesOf(cwd) {
|
|
|
2864
3088
|
return names;
|
|
2865
3089
|
}
|
|
2866
3090
|
function manifestOf(cwd) {
|
|
2867
|
-
const file =
|
|
2868
|
-
if (!
|
|
3091
|
+
const file = join4(cwd, "package.json");
|
|
3092
|
+
if (!existsSync3(file)) {
|
|
2869
3093
|
return void 0;
|
|
2870
3094
|
}
|
|
2871
3095
|
let manifest;
|
|
2872
3096
|
try {
|
|
2873
|
-
manifest = JSON.parse(
|
|
3097
|
+
manifest = JSON.parse(readFileSync4(file, "utf8"));
|
|
2874
3098
|
} catch {
|
|
2875
3099
|
return void 0;
|
|
2876
3100
|
}
|
|
@@ -2886,7 +3110,7 @@ function detectFramework(cwd) {
|
|
|
2886
3110
|
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2887
3111
|
}
|
|
2888
3112
|
}
|
|
2889
|
-
if (
|
|
3113
|
+
if (existsSync3(join4(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2890
3114
|
return detectedFrom(cwd, "Bun", []);
|
|
2891
3115
|
}
|
|
2892
3116
|
return void 0;
|
|
@@ -2912,8 +3136,8 @@ function hasImportsBlock(cwd) {
|
|
|
2912
3136
|
}
|
|
2913
3137
|
|
|
2914
3138
|
// src/commands/init.ts
|
|
2915
|
-
import { existsSync as
|
|
2916
|
-
import { dirname as dirname3, join as
|
|
3139
|
+
import { existsSync as existsSync4, mkdirSync, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
3140
|
+
import { dirname as dirname3, join as join5, resolve as resolve4 } from "path";
|
|
2917
3141
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2918
3142
|
import {
|
|
2919
3143
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
@@ -2921,7 +3145,8 @@ import {
|
|
|
2921
3145
|
loadConfigFrom,
|
|
2922
3146
|
PenvError as PenvError12,
|
|
2923
3147
|
RESERVED_TOKENS,
|
|
2924
|
-
|
|
3148
|
+
SCHEMA_SHAPE_FILE as SCHEMA_SHAPE_FILE2,
|
|
3149
|
+
schemaFileOf as schemaFileOf4,
|
|
2925
3150
|
schemaInsideTree,
|
|
2926
3151
|
validateSchemaFile
|
|
2927
3152
|
} from "@penvhq/core";
|
|
@@ -3065,6 +3290,7 @@ function seamFor(framework, context) {
|
|
|
3065
3290
|
}
|
|
3066
3291
|
|
|
3067
3292
|
// src/commands/init.ts
|
|
3293
|
+
var SCHEMA_SHAPE_BASENAME = "penv.schema";
|
|
3068
3294
|
var CONFIG_FILE = "penv.config.ts";
|
|
3069
3295
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
3070
3296
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -3127,8 +3353,8 @@ function configOf(decisions) {
|
|
|
3127
3353
|
return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
|
|
3128
3354
|
}
|
|
3129
3355
|
function declaredIn(root) {
|
|
3130
|
-
const file =
|
|
3131
|
-
if (!
|
|
3356
|
+
const file = join5(root, CONFIG_FILE);
|
|
3357
|
+
if (!existsSync4(file)) {
|
|
3132
3358
|
return void 0;
|
|
3133
3359
|
}
|
|
3134
3360
|
return loadConfigFrom(file);
|
|
@@ -3151,7 +3377,7 @@ function planInit(root, flags = {}) {
|
|
|
3151
3377
|
);
|
|
3152
3378
|
}
|
|
3153
3379
|
}
|
|
3154
|
-
const schemaFile = flags.schema === void 0 ? declared !== void 0 ?
|
|
3380
|
+
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? schemaFileOf4(declared) : detected?.schemaFile ?? DEFAULT_SCHEMA_FILE2 : flags.schema.trim();
|
|
3155
3381
|
if (flags.schema !== void 0) {
|
|
3156
3382
|
if (schemaFile.length === 0) {
|
|
3157
3383
|
throw emptyFlag("schema");
|
|
@@ -3266,20 +3492,17 @@ async function askInject(io) {
|
|
|
3266
3492
|
}
|
|
3267
3493
|
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.";
|
|
3268
3494
|
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";
|
|
3269
|
-
function
|
|
3495
|
+
function renderSchemaShapeModule(fields, draft) {
|
|
3270
3496
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3271
|
-
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";
|
|
3272
|
-
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3273
3497
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3274
|
-
import { load } from "@penvhq/penv";
|
|
3275
3498
|
|
|
3276
|
-
// The shape
|
|
3277
|
-
// type \u2014 tests, tooling \u2014 so you
|
|
3499
|
+
// The shape \u2014 the one definition every consumer derives from. Import this (or
|
|
3500
|
+
// z.infer<typeof schema>) when you only need the type \u2014 tests, tooling \u2014 so you
|
|
3501
|
+
// don't trigger config loading. .penv/env.ts wraps it with load().
|
|
3278
3502
|
export const schema = z.object({
|
|
3279
3503
|
${body}
|
|
3280
3504
|
});
|
|
3281
3505
|
|
|
3282
|
-
` + loadComment + loadCall + `
|
|
3283
3506
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3284
3507
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3285
3508
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3290,6 +3513,24 @@ declare module "@penvhq/core" {
|
|
|
3290
3513
|
}
|
|
3291
3514
|
`;
|
|
3292
3515
|
}
|
|
3516
|
+
function shapeSpecifier(schemaFile) {
|
|
3517
|
+
const depth = schemaFile.split("/").length - 1;
|
|
3518
|
+
const climb = depth === 0 ? "./" : "../".repeat(depth);
|
|
3519
|
+
return `${climb}${SCHEMA_SHAPE_BASENAME}.js`;
|
|
3520
|
+
}
|
|
3521
|
+
function renderEnvModule(schemaFile, inject = false) {
|
|
3522
|
+
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";
|
|
3523
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true, snapshot });\n" : "export const env = load(schema, { snapshot });\n";
|
|
3524
|
+
return `import { load } from "@penvhq/penv";
|
|
3525
|
+
import { schema } from "${shapeSpecifier(schemaFile)}";
|
|
3526
|
+
import { snapshot } from "${snapshotSpecifier(schemaFile)}";
|
|
3527
|
+
|
|
3528
|
+
// Re-exported so type-only consumers can import the shape through @env without
|
|
3529
|
+
// triggering config loading \u2014 the shape itself lives in ${SCHEMA_SHAPE_FILE2}.
|
|
3530
|
+
export { schema };
|
|
3531
|
+
|
|
3532
|
+
` + loadComment + loadCall;
|
|
3533
|
+
}
|
|
3293
3534
|
function renderEnvironments(decisions) {
|
|
3294
3535
|
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";
|
|
3295
3536
|
if (decisions.environments.length === 0) {
|
|
@@ -3297,7 +3538,7 @@ function renderEnvironments(decisions) {
|
|
|
3297
3538
|
// can read off your codebase, and an environment you do not have is worse
|
|
3298
3539
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
3299
3540
|
// environments: ["development", "production"],
|
|
3300
|
-
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-
|
|
3541
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
|
|
3301
3542
|
environments: [],
|
|
3302
3543
|
|
|
3303
3544
|
providers: {},
|
|
@@ -3554,45 +3795,101 @@ function renderTsconfig(target, alias) {
|
|
|
3554
3795
|
}
|
|
3555
3796
|
function ensurePenvDir(root) {
|
|
3556
3797
|
const dir = resolve4(root, PENV_DIR2);
|
|
3557
|
-
if (
|
|
3798
|
+
if (existsSync4(dir)) {
|
|
3558
3799
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
3559
3800
|
}
|
|
3560
3801
|
mkdirSync(dir, { recursive: true });
|
|
3561
3802
|
return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
|
|
3562
3803
|
}
|
|
3563
|
-
function
|
|
3564
|
-
const file =
|
|
3565
|
-
if (
|
|
3804
|
+
function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
3805
|
+
const file = join5(root, SCHEMA_SHAPE_FILE2);
|
|
3806
|
+
if (existsSync4(file)) {
|
|
3566
3807
|
return {
|
|
3567
3808
|
target: "schema",
|
|
3568
3809
|
action: "kept",
|
|
3810
|
+
text: `Kept ${SCHEMA_SHAPE_FILE2}`,
|
|
3811
|
+
note: "(yours \u2014 penv never regenerates it)"
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3814
|
+
const oldLayout = selfContainedSchemaModule(root, decisions.schemaFile);
|
|
3815
|
+
if (oldLayout !== void 0) {
|
|
3816
|
+
return {
|
|
3817
|
+
target: "schema",
|
|
3818
|
+
action: "conflicted",
|
|
3819
|
+
text: `Kept your schema in ${oldLayout}`,
|
|
3820
|
+
note: `(the shape lives there, from before the ${SCHEMA_SHAPE_FILE2} split \u2014 penv did not add a second one. To adopt the split: move the \`z.object\` (and any \`declare module\` block) into ${SCHEMA_SHAPE_FILE2}, leaving ${oldLayout} importing \`schema\` from it and calling \`load\`.)`
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
writeFileSync3(file, renderSchemaShapeModule(fields, draft), "utf8");
|
|
3824
|
+
return {
|
|
3825
|
+
target: "schema",
|
|
3826
|
+
action: "created",
|
|
3827
|
+
text: `Generated ${SCHEMA_SHAPE_FILE2}`,
|
|
3828
|
+
note: draft ? "(draft schema \u2014 review it, it's yours)" : "(the shape \u2014 yours to edit)"
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
|
|
3832
|
+
const file = join5(root, ...decisions.schemaFile.split("/"));
|
|
3833
|
+
if (existsSync4(file)) {
|
|
3834
|
+
return {
|
|
3835
|
+
target: "env",
|
|
3836
|
+
action: "kept",
|
|
3569
3837
|
text: `Kept ${decisions.schemaFile}`,
|
|
3570
3838
|
note: "(yours \u2014 penv never regenerates it)"
|
|
3571
3839
|
};
|
|
3572
3840
|
}
|
|
3573
3841
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3574
|
-
|
|
3842
|
+
writeFileSync3(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
|
|
3575
3843
|
return {
|
|
3576
|
-
target: "
|
|
3844
|
+
target: "env",
|
|
3577
3845
|
action: "created",
|
|
3578
3846
|
text: `Generated ${decisions.schemaFile}`,
|
|
3579
|
-
note:
|
|
3847
|
+
note: "(loads the shape \u2014 yours to edit)"
|
|
3580
3848
|
};
|
|
3581
3849
|
}
|
|
3582
3850
|
function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
3583
|
-
const file =
|
|
3584
|
-
if (
|
|
3851
|
+
const file = join5(root, CONFIG_FILE);
|
|
3852
|
+
if (existsSync4(file)) {
|
|
3585
3853
|
return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
|
|
3586
3854
|
}
|
|
3587
|
-
|
|
3855
|
+
writeFileSync3(file, renderConfigModule(decisions), "utf8");
|
|
3588
3856
|
return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
|
|
3589
3857
|
}
|
|
3858
|
+
function snapshotConfig(decisions) {
|
|
3859
|
+
const providers = {};
|
|
3860
|
+
for (const environment of decisions.environments) {
|
|
3861
|
+
providers[environment] = { type: "@penvhq/provider-filesystem" };
|
|
3862
|
+
}
|
|
3863
|
+
return {
|
|
3864
|
+
environments: decisions.environments,
|
|
3865
|
+
providers,
|
|
3866
|
+
...decisions.schemaFile === DEFAULT_SCHEMA_FILE2 ? {} : { schemaFile: decisions.schemaFile },
|
|
3867
|
+
...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: decisions.publicPrefixes }
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
function writeSnapshotModule(root, decisions = DEFAULT_DECISIONS) {
|
|
3871
|
+
const file = join5(root, SNAPSHOT_FILE);
|
|
3872
|
+
if (existsSync4(file)) {
|
|
3873
|
+
return { target: "snapshot", action: "kept", text: `Kept ${SNAPSHOT_FILE}` };
|
|
3874
|
+
}
|
|
3875
|
+
writeFileSync3(
|
|
3876
|
+
file,
|
|
3877
|
+
renderSnapshotModule(buildSnapshotFrom(root, snapshotConfig(decisions))),
|
|
3878
|
+
"utf8"
|
|
3879
|
+
);
|
|
3880
|
+
return {
|
|
3881
|
+
target: "snapshot",
|
|
3882
|
+
action: "created",
|
|
3883
|
+
text: `Generated ${SNAPSHOT_FILE}`,
|
|
3884
|
+
note: "(committed \u2014 the bundle resolves load() from it)"
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3590
3887
|
function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
3591
3888
|
const alias = decisions.alias;
|
|
3592
3889
|
const imports = alias.startsWith(IMPORTS_PREFIX);
|
|
3593
|
-
const file =
|
|
3890
|
+
const file = join5(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
|
|
3594
3891
|
const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
|
|
3595
|
-
if (!
|
|
3892
|
+
if (!existsSync4(file)) {
|
|
3596
3893
|
if (imports) {
|
|
3597
3894
|
return {
|
|
3598
3895
|
target: "tsconfig",
|
|
@@ -3601,14 +3898,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3601
3898
|
note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
|
|
3602
3899
|
};
|
|
3603
3900
|
}
|
|
3604
|
-
|
|
3901
|
+
writeFileSync3(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
|
|
3605
3902
|
return {
|
|
3606
3903
|
target: "tsconfig",
|
|
3607
3904
|
action: "created",
|
|
3608
3905
|
text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
|
|
3609
3906
|
};
|
|
3610
3907
|
}
|
|
3611
|
-
const source =
|
|
3908
|
+
const source = readFileSync5(file, "utf8");
|
|
3612
3909
|
const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
|
|
3613
3910
|
if (edit.conflict !== void 0) {
|
|
3614
3911
|
return {
|
|
@@ -3625,7 +3922,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3625
3922
|
text: `Kept the ${alias} alias in ${where}`
|
|
3626
3923
|
};
|
|
3627
3924
|
}
|
|
3628
|
-
|
|
3925
|
+
writeFileSync3(file, edit.source, "utf8");
|
|
3629
3926
|
return {
|
|
3630
3927
|
target: "tsconfig",
|
|
3631
3928
|
action: "updated",
|
|
@@ -3633,15 +3930,15 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3633
3930
|
};
|
|
3634
3931
|
}
|
|
3635
3932
|
function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
3636
|
-
const file =
|
|
3933
|
+
const file = join5(root, PENV_DIR2, GITIGNORE_FILE);
|
|
3637
3934
|
const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
|
|
3638
3935
|
const wanted = renderGitignore(decisions);
|
|
3639
|
-
const existing =
|
|
3936
|
+
const existing = existsSync4(file) ? readFileSync5(file, "utf8") : void 0;
|
|
3640
3937
|
if (existing === wanted) {
|
|
3641
3938
|
return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
|
|
3642
3939
|
}
|
|
3643
|
-
mkdirSync(
|
|
3644
|
-
|
|
3940
|
+
mkdirSync(join5(root, PENV_DIR2), { recursive: true });
|
|
3941
|
+
writeFileSync3(file, wanted, "utf8");
|
|
3645
3942
|
return {
|
|
3646
3943
|
target: "gitignore",
|
|
3647
3944
|
action: existing === void 0 ? "created" : "updated",
|
|
@@ -3657,12 +3954,12 @@ function outdatedRuntimeWarning(root) {
|
|
|
3657
3954
|
return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
|
|
3658
3955
|
}
|
|
3659
3956
|
function installedPenvVersion(root) {
|
|
3660
|
-
const file =
|
|
3661
|
-
if (!
|
|
3957
|
+
const file = join5(root, "node_modules", "@penvhq", "penv", "package.json");
|
|
3958
|
+
if (!existsSync4(file)) {
|
|
3662
3959
|
return void 0;
|
|
3663
3960
|
}
|
|
3664
3961
|
try {
|
|
3665
|
-
const version = JSON.parse(
|
|
3962
|
+
const version = JSON.parse(readFileSync5(file, "utf8")).version;
|
|
3666
3963
|
return typeof version === "string" ? version : void 0;
|
|
3667
3964
|
} catch {
|
|
3668
3965
|
return void 0;
|
|
@@ -3702,11 +3999,11 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramew
|
|
|
3702
3999
|
};
|
|
3703
4000
|
}
|
|
3704
4001
|
const alsoNote = writeAlso(root, seam.also);
|
|
3705
|
-
const file =
|
|
4002
|
+
const file = join5(root, ...seam.file.split("/"));
|
|
3706
4003
|
const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
|
|
3707
4004
|
const notes = baseNotes.length === 0 ? "" : `
|
|
3708
4005
|
${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
3709
|
-
if (
|
|
4006
|
+
if (existsSync4(file)) {
|
|
3710
4007
|
return {
|
|
3711
4008
|
target: "seam",
|
|
3712
4009
|
action: "info",
|
|
@@ -3715,7 +4012,7 @@ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
|
3715
4012
|
};
|
|
3716
4013
|
}
|
|
3717
4014
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3718
|
-
|
|
4015
|
+
writeFileSync3(file, seam.content, "utf8");
|
|
3719
4016
|
return {
|
|
3720
4017
|
target: "seam",
|
|
3721
4018
|
action: outdated === void 0 ? "created" : "info",
|
|
@@ -3727,12 +4024,12 @@ function writeAlso(root, also) {
|
|
|
3727
4024
|
if (also === void 0) {
|
|
3728
4025
|
return void 0;
|
|
3729
4026
|
}
|
|
3730
|
-
const file =
|
|
3731
|
-
if (
|
|
4027
|
+
const file = join5(root, ...also.file.split("/"));
|
|
4028
|
+
if (existsSync4(file)) {
|
|
3732
4029
|
return also.ifPresent;
|
|
3733
4030
|
}
|
|
3734
4031
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3735
|
-
|
|
4032
|
+
writeFileSync3(file, also.content, "utf8");
|
|
3736
4033
|
return `Wrote ${also.file} to register it.`;
|
|
3737
4034
|
}
|
|
3738
4035
|
function withWarning(note, warning) {
|
|
@@ -3745,8 +4042,15 @@ function withWarning(note, warning) {
|
|
|
3745
4042
|
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3746
4043
|
const steps = [
|
|
3747
4044
|
ensurePenvDir(root),
|
|
3748
|
-
|
|
4045
|
+
// The shape at the root, then the wrapper that loads it — both write-once.
|
|
4046
|
+
// The shape step needs the decisions too: an old-layout `schemaFile` already
|
|
4047
|
+
// holds the shape, and a second `penv.schema.ts` beside it would not compile.
|
|
4048
|
+
writeSchemaShapeFile(root, fields, draft, decisions),
|
|
4049
|
+
writeEnvFile(root, decisions),
|
|
3749
4050
|
writeConfigFile(root, decisions),
|
|
4051
|
+
// After the config, whose evaluated shape the snapshot embeds; the wrapper
|
|
4052
|
+
// env.ts written above already imports it.
|
|
4053
|
+
writeSnapshotModule(root, decisions),
|
|
3750
4054
|
writeTsconfigAlias(root, decisions),
|
|
3751
4055
|
writeGitignore(root, decisions)
|
|
3752
4056
|
];
|
|
@@ -3763,10 +4067,12 @@ function renderInit(result2) {
|
|
|
3763
4067
|
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3764
4068
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3765
4069
|
});
|
|
4070
|
+
const schemaStep = result2.steps.find((step) => step.target === "schema");
|
|
4071
|
+
const shapeFile = schemaStep?.action === "conflicted" ? result2.decisions.schemaFile : SCHEMA_SHAPE_FILE2;
|
|
3766
4072
|
return [
|
|
3767
4073
|
...formatSteps(steps),
|
|
3768
4074
|
"",
|
|
3769
|
-
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${
|
|
4075
|
+
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${shapeFile}, then:`,
|
|
3770
4076
|
tip(out.cyan("penv set <key>")),
|
|
3771
4077
|
...result2.decisions.environments.length === 0 ? [
|
|
3772
4078
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
@@ -4004,7 +4310,7 @@ function decisionsOf(config, cwd) {
|
|
|
4004
4310
|
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
4005
4311
|
// choice, so it is not turned on here.
|
|
4006
4312
|
inject: false,
|
|
4007
|
-
schemaFile:
|
|
4313
|
+
schemaFile: schemaFileOf5(config),
|
|
4008
4314
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
4009
4315
|
alias: detectAlias(cwd)
|
|
4010
4316
|
};
|
|
@@ -4026,14 +4332,14 @@ function configInEffect(cwd, environment) {
|
|
|
4026
4332
|
function importDotenv(options) {
|
|
4027
4333
|
const cwd = resolve5(options.cwd);
|
|
4028
4334
|
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
4029
|
-
if (!
|
|
4335
|
+
if (!existsSync5(file)) {
|
|
4030
4336
|
throw new PenvError13(
|
|
4031
4337
|
"IMPORT_FILE_MISSING",
|
|
4032
4338
|
`There is no file at ${file} to import`,
|
|
4033
4339
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
4034
4340
|
);
|
|
4035
4341
|
}
|
|
4036
|
-
const parsed = parseDotenv(
|
|
4342
|
+
const parsed = parseDotenv(readFileSync6(file, "utf8"));
|
|
4037
4343
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
4038
4344
|
const source = displayPath2(cwd, file);
|
|
4039
4345
|
const named = scopeFromFilename(file, config);
|
|
@@ -4071,6 +4377,7 @@ function importDotenv(options) {
|
|
|
4071
4377
|
}
|
|
4072
4378
|
const backup = `${file}${BACKUP_SUFFIX}`;
|
|
4073
4379
|
copyFileSync(file, backup);
|
|
4380
|
+
refreshSnapshot(project);
|
|
4074
4381
|
return {
|
|
4075
4382
|
root: project.root,
|
|
4076
4383
|
file,
|
|
@@ -4383,7 +4690,7 @@ var listCommand = defineCommand12({
|
|
|
4383
4690
|
import {
|
|
4384
4691
|
accessPath as accessPath5,
|
|
4385
4692
|
formatMetaFile,
|
|
4386
|
-
formatValueFile as
|
|
4693
|
+
formatValueFile as formatValueFile6,
|
|
4387
4694
|
openValue as openValue4,
|
|
4388
4695
|
PenvError as PenvError15,
|
|
4389
4696
|
parameterId as parameterId5,
|
|
@@ -4406,7 +4713,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
4406
4713
|
if (environment === void 0) {
|
|
4407
4714
|
throw new PenvError15(
|
|
4408
4715
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
4409
|
-
`${PENV_DIR}/${
|
|
4716
|
+
`${PENV_DIR}/${formatValueFile6(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
4410
4717
|
"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."
|
|
4411
4718
|
);
|
|
4412
4719
|
}
|
|
@@ -4415,7 +4722,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
4415
4722
|
if (opened.kind === "failed") {
|
|
4416
4723
|
throw new PenvError15(
|
|
4417
4724
|
"VALUE_UNDECRYPTABLE",
|
|
4418
|
-
`${PENV_DIR}/${
|
|
4725
|
+
`${PENV_DIR}/${formatValueFile6(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
4419
4726
|
"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."
|
|
4420
4727
|
);
|
|
4421
4728
|
}
|
|
@@ -4480,19 +4787,21 @@ async function runMove(options) {
|
|
|
4480
4787
|
if (meta !== void 0) {
|
|
4481
4788
|
await project.provider.removeMeta(from);
|
|
4482
4789
|
}
|
|
4790
|
+
refreshSnapshot(project);
|
|
4483
4791
|
return {
|
|
4484
4792
|
from: parameterId5(from),
|
|
4485
4793
|
to: parameterId5(to),
|
|
4486
4794
|
files: planned.map((file) => ({
|
|
4487
|
-
from:
|
|
4488
|
-
to:
|
|
4795
|
+
from: formatValueFile6(file.source),
|
|
4796
|
+
to: formatValueFile6(file.target),
|
|
4489
4797
|
resealed: file.resealed
|
|
4490
4798
|
})),
|
|
4491
4799
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
4492
4800
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
4493
4801
|
// that printed the first while moving the second names no file on disk.
|
|
4494
4802
|
meta: meta === void 0 ? void 0 : formatMetaFile({ ...to, format: "json" }),
|
|
4495
|
-
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
|
|
4803
|
+
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") },
|
|
4804
|
+
schemaFile: schemaShapeFileOf(project)
|
|
4496
4805
|
};
|
|
4497
4806
|
}
|
|
4498
4807
|
function renderMove(result2) {
|
|
@@ -4509,7 +4818,7 @@ function renderMove(result2) {
|
|
|
4509
4818
|
lines.push(
|
|
4510
4819
|
"",
|
|
4511
4820
|
tip(
|
|
4512
|
-
|
|
4821
|
+
`${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.`
|
|
4513
4822
|
)
|
|
4514
4823
|
);
|
|
4515
4824
|
return lines;
|
|
@@ -4596,6 +4905,7 @@ async function runPull(options) {
|
|
|
4596
4905
|
tree.writeMetaSync(ref, block);
|
|
4597
4906
|
meta += 1;
|
|
4598
4907
|
}
|
|
4908
|
+
refreshSnapshot(project);
|
|
4599
4909
|
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
4600
4910
|
}
|
|
4601
4911
|
function renderPull(result2) {
|
|
@@ -4664,7 +4974,7 @@ var pullCommand = defineCommand14({
|
|
|
4664
4974
|
});
|
|
4665
4975
|
|
|
4666
4976
|
// src/commands/remove.ts
|
|
4667
|
-
import { formatValueFile as
|
|
4977
|
+
import { formatValueFile as formatValueFile7 } from "@penvhq/core";
|
|
4668
4978
|
import { defineCommand as defineCommand15 } from "citty";
|
|
4669
4979
|
async function runRemove(options) {
|
|
4670
4980
|
const project = openProject(options.cwd);
|
|
@@ -4682,12 +4992,13 @@ async function runRemove(options) {
|
|
|
4682
4992
|
continue;
|
|
4683
4993
|
}
|
|
4684
4994
|
await project.provider.remove(file);
|
|
4685
|
-
removed.push(
|
|
4995
|
+
removed.push(formatValueFile7(file));
|
|
4686
4996
|
}
|
|
4997
|
+
refreshSnapshot(project);
|
|
4687
4998
|
return {
|
|
4688
4999
|
parameter: options.key,
|
|
4689
5000
|
removed,
|
|
4690
|
-
considered: files.map((file) =>
|
|
5001
|
+
considered: files.map((file) => formatValueFile7(file))
|
|
4691
5002
|
};
|
|
4692
5003
|
}
|
|
4693
5004
|
function renderRemove(result2) {
|
|
@@ -4763,6 +5074,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
4763
5074
|
value,
|
|
4764
5075
|
environment
|
|
4765
5076
|
});
|
|
5077
|
+
refreshSnapshot(project);
|
|
4766
5078
|
return;
|
|
4767
5079
|
}
|
|
4768
5080
|
await provider.write(rotatingFile(ref, environment), value);
|
|
@@ -4930,11 +5242,53 @@ var rotateCommand = defineCommand16({
|
|
|
4930
5242
|
}
|
|
4931
5243
|
});
|
|
4932
5244
|
|
|
5245
|
+
// src/commands/snapshot.ts
|
|
5246
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
5247
|
+
function runSnapshot(options) {
|
|
5248
|
+
const project = openProject(options.cwd);
|
|
5249
|
+
return { write: writeSnapshotFile(project), wire: wireEnvModule(project) };
|
|
5250
|
+
}
|
|
5251
|
+
function renderSnapshot(result2) {
|
|
5252
|
+
const label = result2.write.action === "created" ? "Generated" : result2.write.action === "updated" ? "Updated" : "Up to date";
|
|
5253
|
+
const rows = [{ glyph: CHECK, label, subject: SNAPSHOT_FILE }];
|
|
5254
|
+
if (result2.wire.action === "wired") {
|
|
5255
|
+
rows.push({
|
|
5256
|
+
glyph: CHECK,
|
|
5257
|
+
label: "Wired",
|
|
5258
|
+
subject: result2.wire.file,
|
|
5259
|
+
detail: "load reads the snapshot"
|
|
5260
|
+
});
|
|
5261
|
+
} else if (result2.wire.action === "kept") {
|
|
5262
|
+
rows.push({ glyph: CHECK, label: "Wired", subject: result2.wire.file });
|
|
5263
|
+
}
|
|
5264
|
+
const lines = formatRows(rows);
|
|
5265
|
+
if (result2.wire.action === "manual") {
|
|
5266
|
+
lines.push(
|
|
5267
|
+
"",
|
|
5268
|
+
`${WARN} penv could not wire ${result2.wire.file} automatically \u2014 add these two lines yourself:`,
|
|
5269
|
+
tip(result2.wire.importLine),
|
|
5270
|
+
tip(result2.wire.loadHint)
|
|
5271
|
+
);
|
|
5272
|
+
}
|
|
5273
|
+
return lines;
|
|
5274
|
+
}
|
|
5275
|
+
var snapshotCommand = defineCommand17({
|
|
5276
|
+
meta: {
|
|
5277
|
+
name: "snapshot",
|
|
5278
|
+
description: "Generate penv.snapshot.ts so load() resolves in a bundled/serverless runtime"
|
|
5279
|
+
},
|
|
5280
|
+
run() {
|
|
5281
|
+
return guard(async () => {
|
|
5282
|
+
write(renderSnapshot(runSnapshot({ cwd: process.cwd() })));
|
|
5283
|
+
});
|
|
5284
|
+
}
|
|
5285
|
+
});
|
|
5286
|
+
|
|
4933
5287
|
// src/commands/watch.ts
|
|
4934
|
-
import { existsSync as
|
|
5288
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
4935
5289
|
import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
|
|
4936
|
-
import { schemaFileOf as
|
|
4937
|
-
import { defineCommand as
|
|
5290
|
+
import { SCHEMA_SHAPE_FILE as SCHEMA_SHAPE_FILE3, schemaFileOf as schemaFileOf6, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
|
|
5291
|
+
import { defineCommand as defineCommand18 } from "citty";
|
|
4938
5292
|
var DEBOUNCE_MS = 100;
|
|
4939
5293
|
function runWatch(options) {
|
|
4940
5294
|
const project = openProject(options.cwd);
|
|
@@ -4999,14 +5353,14 @@ function runWatch(options) {
|
|
|
4999
5353
|
if (closed || recovery === void 0) {
|
|
5000
5354
|
return;
|
|
5001
5355
|
}
|
|
5002
|
-
if (!
|
|
5356
|
+
if (!existsSync6(parent)) {
|
|
5003
5357
|
stop(recovery);
|
|
5004
5358
|
return;
|
|
5005
5359
|
}
|
|
5006
5360
|
if (filename !== null && basename2(filename) !== name) {
|
|
5007
5361
|
return;
|
|
5008
5362
|
}
|
|
5009
|
-
if (!
|
|
5363
|
+
if (!existsSync6(target)) {
|
|
5010
5364
|
return;
|
|
5011
5365
|
}
|
|
5012
5366
|
stop(recovery);
|
|
@@ -5030,7 +5384,7 @@ function runWatch(options) {
|
|
|
5030
5384
|
if (closed) {
|
|
5031
5385
|
return;
|
|
5032
5386
|
}
|
|
5033
|
-
if (!
|
|
5387
|
+
if (!existsSync6(target)) {
|
|
5034
5388
|
if (watcher !== void 0) {
|
|
5035
5389
|
stop(watcher);
|
|
5036
5390
|
}
|
|
@@ -5066,8 +5420,9 @@ function runWatch(options) {
|
|
|
5066
5420
|
}
|
|
5067
5421
|
addWatcher(project.penvDir, true);
|
|
5068
5422
|
addWatcher(dirname4(project.configFile), false, configFile);
|
|
5423
|
+
addWatcher(project.root, false, SCHEMA_SHAPE_FILE3);
|
|
5069
5424
|
if (schemaInsideTree2(project.config) === void 0) {
|
|
5070
|
-
const schemaFile = resolve6(project.root,
|
|
5425
|
+
const schemaFile = resolve6(project.root, schemaFileOf6(project.config));
|
|
5071
5426
|
addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
|
|
5072
5427
|
}
|
|
5073
5428
|
void validate();
|
|
@@ -5111,7 +5466,7 @@ function renderDrift(drift, environment) {
|
|
|
5111
5466
|
}
|
|
5112
5467
|
return lines;
|
|
5113
5468
|
}
|
|
5114
|
-
var watchCommand =
|
|
5469
|
+
var watchCommand = defineCommand18({
|
|
5115
5470
|
meta: {
|
|
5116
5471
|
name: "watch",
|
|
5117
5472
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -5150,7 +5505,7 @@ var watchCommand = defineCommand17({
|
|
|
5150
5505
|
});
|
|
5151
5506
|
|
|
5152
5507
|
// src/index.ts
|
|
5153
|
-
var main =
|
|
5508
|
+
var main = defineCommand19({
|
|
5154
5509
|
meta: {
|
|
5155
5510
|
name: "penv",
|
|
5156
5511
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -5171,6 +5526,7 @@ var main = defineCommand18({
|
|
|
5171
5526
|
encrypt: encryptCommand,
|
|
5172
5527
|
decrypt: decryptCommand,
|
|
5173
5528
|
key: keyCommand,
|
|
5529
|
+
snapshot: snapshotCommand,
|
|
5174
5530
|
validate: validateCommand,
|
|
5175
5531
|
doctor: doctorCommand,
|
|
5176
5532
|
watch: watchCommand
|
|
@@ -5192,6 +5548,7 @@ export {
|
|
|
5192
5548
|
renderPull,
|
|
5193
5549
|
renderPush,
|
|
5194
5550
|
renderRotate,
|
|
5551
|
+
renderSnapshot,
|
|
5195
5552
|
renderWatch,
|
|
5196
5553
|
runDecrypt,
|
|
5197
5554
|
runDoctor,
|
|
@@ -5209,6 +5566,7 @@ export {
|
|
|
5209
5566
|
runRemove,
|
|
5210
5567
|
runRotate,
|
|
5211
5568
|
runSet,
|
|
5569
|
+
runSnapshot,
|
|
5212
5570
|
runValidate,
|
|
5213
5571
|
runWatch
|
|
5214
5572
|
};
|