@penvhq/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +758 -322
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +573 -136
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.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,11 +3053,11 @@ 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
|
-
const dir =
|
|
3060
|
+
const dir = srcPrefix(cwd);
|
|
2837
3061
|
const preferred = `${dir}env.ts`;
|
|
2838
3062
|
if (!occupied(cwd, preferred)) {
|
|
2839
3063
|
return { file: preferred };
|
|
@@ -2844,6 +3068,9 @@ function schemaFileFor(cwd) {
|
|
|
2844
3068
|
}
|
|
2845
3069
|
return { file: DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2846
3070
|
}
|
|
3071
|
+
function srcPrefix(cwd) {
|
|
3072
|
+
return existsSync3(join4(cwd, "src")) ? "src/" : "";
|
|
3073
|
+
}
|
|
2847
3074
|
function dependenciesOf(cwd) {
|
|
2848
3075
|
const manifest = manifestOf(cwd);
|
|
2849
3076
|
if (manifest === void 0) {
|
|
@@ -2861,13 +3088,13 @@ function dependenciesOf(cwd) {
|
|
|
2861
3088
|
return names;
|
|
2862
3089
|
}
|
|
2863
3090
|
function manifestOf(cwd) {
|
|
2864
|
-
const file =
|
|
2865
|
-
if (!
|
|
3091
|
+
const file = join4(cwd, "package.json");
|
|
3092
|
+
if (!existsSync3(file)) {
|
|
2866
3093
|
return void 0;
|
|
2867
3094
|
}
|
|
2868
3095
|
let manifest;
|
|
2869
3096
|
try {
|
|
2870
|
-
manifest = JSON.parse(
|
|
3097
|
+
manifest = JSON.parse(readFileSync4(file, "utf8"));
|
|
2871
3098
|
} catch {
|
|
2872
3099
|
return void 0;
|
|
2873
3100
|
}
|
|
@@ -2883,7 +3110,7 @@ function detectFramework(cwd) {
|
|
|
2883
3110
|
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2884
3111
|
}
|
|
2885
3112
|
}
|
|
2886
|
-
if (
|
|
3113
|
+
if (existsSync3(join4(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2887
3114
|
return detectedFrom(cwd, "Bun", []);
|
|
2888
3115
|
}
|
|
2889
3116
|
return void 0;
|
|
@@ -2909,8 +3136,8 @@ function hasImportsBlock(cwd) {
|
|
|
2909
3136
|
}
|
|
2910
3137
|
|
|
2911
3138
|
// src/commands/init.ts
|
|
2912
|
-
import { existsSync as
|
|
2913
|
-
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";
|
|
2914
3141
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2915
3142
|
import {
|
|
2916
3143
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
@@ -2918,7 +3145,8 @@ import {
|
|
|
2918
3145
|
loadConfigFrom,
|
|
2919
3146
|
PenvError as PenvError12,
|
|
2920
3147
|
RESERVED_TOKENS,
|
|
2921
|
-
|
|
3148
|
+
SCHEMA_SHAPE_FILE as SCHEMA_SHAPE_FILE2,
|
|
3149
|
+
schemaFileOf as schemaFileOf4,
|
|
2922
3150
|
schemaInsideTree,
|
|
2923
3151
|
validateSchemaFile
|
|
2924
3152
|
} from "@penvhq/core";
|
|
@@ -2990,19 +3218,25 @@ function bun({ alias }) {
|
|
|
2990
3218
|
kind: "scaffold",
|
|
2991
3219
|
file: ".penv/preload.ts",
|
|
2992
3220
|
content: `// Bun evaluates this file before your entry point, because it is listed under
|
|
2993
|
-
// \`preload\` in bunfig.toml
|
|
2994
|
-
//
|
|
3221
|
+
// \`preload\` in bunfig.toml. Running it injects your config into process.env
|
|
3222
|
+
// before any library reads it.
|
|
2995
3223
|
import "${alias}";
|
|
2996
3224
|
`,
|
|
2997
3225
|
ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
|
|
2998
|
-
notes: [
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3226
|
+
notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
|
|
3227
|
+
// The preload file is inert until bunfig.toml points at it — so penv writes
|
|
3228
|
+
// that too, both for the runtime and for `bun test`. Only when there is no
|
|
3229
|
+
// bunfig to overwrite; an existing one is the user's, and penv prints instead.
|
|
3230
|
+
also: {
|
|
3231
|
+
file: "bunfig.toml",
|
|
3232
|
+
content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
|
|
3233
|
+
ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
|
|
3234
|
+
}
|
|
3002
3235
|
};
|
|
3003
3236
|
}
|
|
3004
|
-
function node({ alias,
|
|
3005
|
-
const
|
|
3237
|
+
function node({ alias, schemaFile }) {
|
|
3238
|
+
const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
|
|
3239
|
+
const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
|
|
3006
3240
|
return {
|
|
3007
3241
|
kind: "instruct",
|
|
3008
3242
|
instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
|
|
@@ -3022,10 +3256,17 @@ import before it. If you have no server.ts yet, create it from TanStack's server
|
|
|
3022
3256
|
template and put that import first.`
|
|
3023
3257
|
};
|
|
3024
3258
|
}
|
|
3025
|
-
function astro(
|
|
3259
|
+
function astro({ alias }) {
|
|
3026
3260
|
return {
|
|
3027
3261
|
kind: "instruct",
|
|
3028
|
-
instruction:
|
|
3262
|
+
instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
|
|
3263
|
+
injects the import into every SSR page, then cover the routes it cannot reach:
|
|
3264
|
+
\u2022 In astro.config integrations, add:
|
|
3265
|
+
{ name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
|
|
3266
|
+
injectScript("page-ssr", 'import "${alias}";') } }
|
|
3267
|
+
\u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
|
|
3268
|
+
endpoint route (src/pages/*.ts) that imports a library reading process.env.
|
|
3269
|
+
Injection only applies under a server (on-demand) adapter, not a static build.`
|
|
3029
3270
|
};
|
|
3030
3271
|
}
|
|
3031
3272
|
function viteSpa(_context) {
|
|
@@ -3049,6 +3290,7 @@ function seamFor(framework, context) {
|
|
|
3049
3290
|
}
|
|
3050
3291
|
|
|
3051
3292
|
// src/commands/init.ts
|
|
3293
|
+
var SCHEMA_SHAPE_BASENAME = "penv.schema";
|
|
3052
3294
|
var CONFIG_FILE = "penv.config.ts";
|
|
3053
3295
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
3054
3296
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -3111,8 +3353,8 @@ function configOf(decisions) {
|
|
|
3111
3353
|
return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
|
|
3112
3354
|
}
|
|
3113
3355
|
function declaredIn(root) {
|
|
3114
|
-
const file =
|
|
3115
|
-
if (!
|
|
3356
|
+
const file = join5(root, CONFIG_FILE);
|
|
3357
|
+
if (!existsSync4(file)) {
|
|
3116
3358
|
return void 0;
|
|
3117
3359
|
}
|
|
3118
3360
|
return loadConfigFrom(file);
|
|
@@ -3135,7 +3377,7 @@ function planInit(root, flags = {}) {
|
|
|
3135
3377
|
);
|
|
3136
3378
|
}
|
|
3137
3379
|
}
|
|
3138
|
-
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();
|
|
3139
3381
|
if (flags.schema !== void 0) {
|
|
3140
3382
|
if (schemaFile.length === 0) {
|
|
3141
3383
|
throw emptyFlag("schema");
|
|
@@ -3226,17 +3468,18 @@ async function promptForDecisions(plan2, io) {
|
|
|
3226
3468
|
);
|
|
3227
3469
|
io.write("");
|
|
3228
3470
|
}
|
|
3229
|
-
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
3230
3471
|
const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
|
|
3231
3472
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
3232
3473
|
return void 0;
|
|
3233
3474
|
}
|
|
3475
|
+
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
3234
3476
|
return { ...plan2.decisions, environments, inject };
|
|
3235
3477
|
}
|
|
3236
3478
|
function seamKindFor(plan2) {
|
|
3237
3479
|
return seamFor(plan2.detected?.name, {
|
|
3238
3480
|
alias: plan2.decisions.alias,
|
|
3239
|
-
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
|
|
3481
|
+
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
|
|
3482
|
+
schemaFile: plan2.decisions.schemaFile
|
|
3240
3483
|
}).kind;
|
|
3241
3484
|
}
|
|
3242
3485
|
async function askInject(io) {
|
|
@@ -3249,20 +3492,17 @@ async function askInject(io) {
|
|
|
3249
3492
|
}
|
|
3250
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.";
|
|
3251
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";
|
|
3252
|
-
function
|
|
3495
|
+
function renderSchemaShapeModule(fields, draft) {
|
|
3253
3496
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3254
|
-
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";
|
|
3255
|
-
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3256
3497
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3257
|
-
import { load } from "@penvhq/penv";
|
|
3258
3498
|
|
|
3259
|
-
// The shape
|
|
3260
|
-
// 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().
|
|
3261
3502
|
export const schema = z.object({
|
|
3262
3503
|
${body}
|
|
3263
3504
|
});
|
|
3264
3505
|
|
|
3265
|
-
` + loadComment + loadCall + `
|
|
3266
3506
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3267
3507
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3268
3508
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3273,6 +3513,24 @@ declare module "@penvhq/core" {
|
|
|
3273
3513
|
}
|
|
3274
3514
|
`;
|
|
3275
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
|
+
}
|
|
3276
3534
|
function renderEnvironments(decisions) {
|
|
3277
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";
|
|
3278
3536
|
if (decisions.environments.length === 0) {
|
|
@@ -3280,7 +3538,7 @@ function renderEnvironments(decisions) {
|
|
|
3280
3538
|
// can read off your codebase, and an environment you do not have is worse
|
|
3281
3539
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
3282
3540
|
// environments: ["development", "production"],
|
|
3283
|
-
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-
|
|
3541
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
|
|
3284
3542
|
environments: [],
|
|
3285
3543
|
|
|
3286
3544
|
providers: {},
|
|
@@ -3537,45 +3795,101 @@ function renderTsconfig(target, alias) {
|
|
|
3537
3795
|
}
|
|
3538
3796
|
function ensurePenvDir(root) {
|
|
3539
3797
|
const dir = resolve4(root, PENV_DIR2);
|
|
3540
|
-
if (
|
|
3798
|
+
if (existsSync4(dir)) {
|
|
3541
3799
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
3542
3800
|
}
|
|
3543
3801
|
mkdirSync(dir, { recursive: true });
|
|
3544
3802
|
return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
|
|
3545
3803
|
}
|
|
3546
|
-
function
|
|
3547
|
-
const file =
|
|
3548
|
-
if (
|
|
3804
|
+
function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
3805
|
+
const file = join5(root, SCHEMA_SHAPE_FILE2);
|
|
3806
|
+
if (existsSync4(file)) {
|
|
3807
|
+
return {
|
|
3808
|
+
target: "schema",
|
|
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) {
|
|
3549
3816
|
return {
|
|
3550
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",
|
|
3551
3836
|
action: "kept",
|
|
3552
3837
|
text: `Kept ${decisions.schemaFile}`,
|
|
3553
3838
|
note: "(yours \u2014 penv never regenerates it)"
|
|
3554
3839
|
};
|
|
3555
3840
|
}
|
|
3556
3841
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3557
|
-
|
|
3842
|
+
writeFileSync3(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
|
|
3558
3843
|
return {
|
|
3559
|
-
target: "
|
|
3844
|
+
target: "env",
|
|
3560
3845
|
action: "created",
|
|
3561
3846
|
text: `Generated ${decisions.schemaFile}`,
|
|
3562
|
-
note:
|
|
3847
|
+
note: "(loads the shape \u2014 yours to edit)"
|
|
3563
3848
|
};
|
|
3564
3849
|
}
|
|
3565
3850
|
function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
3566
|
-
const file =
|
|
3567
|
-
if (
|
|
3851
|
+
const file = join5(root, CONFIG_FILE);
|
|
3852
|
+
if (existsSync4(file)) {
|
|
3568
3853
|
return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
|
|
3569
3854
|
}
|
|
3570
|
-
|
|
3855
|
+
writeFileSync3(file, renderConfigModule(decisions), "utf8");
|
|
3571
3856
|
return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
|
|
3572
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
|
+
}
|
|
3573
3887
|
function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
3574
3888
|
const alias = decisions.alias;
|
|
3575
3889
|
const imports = alias.startsWith(IMPORTS_PREFIX);
|
|
3576
|
-
const file =
|
|
3890
|
+
const file = join5(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
|
|
3577
3891
|
const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
|
|
3578
|
-
if (!
|
|
3892
|
+
if (!existsSync4(file)) {
|
|
3579
3893
|
if (imports) {
|
|
3580
3894
|
return {
|
|
3581
3895
|
target: "tsconfig",
|
|
@@ -3584,14 +3898,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3584
3898
|
note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
|
|
3585
3899
|
};
|
|
3586
3900
|
}
|
|
3587
|
-
|
|
3901
|
+
writeFileSync3(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
|
|
3588
3902
|
return {
|
|
3589
3903
|
target: "tsconfig",
|
|
3590
3904
|
action: "created",
|
|
3591
3905
|
text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
|
|
3592
3906
|
};
|
|
3593
3907
|
}
|
|
3594
|
-
const source =
|
|
3908
|
+
const source = readFileSync5(file, "utf8");
|
|
3595
3909
|
const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
|
|
3596
3910
|
if (edit.conflict !== void 0) {
|
|
3597
3911
|
return {
|
|
@@ -3608,7 +3922,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3608
3922
|
text: `Kept the ${alias} alias in ${where}`
|
|
3609
3923
|
};
|
|
3610
3924
|
}
|
|
3611
|
-
|
|
3925
|
+
writeFileSync3(file, edit.source, "utf8");
|
|
3612
3926
|
return {
|
|
3613
3927
|
target: "tsconfig",
|
|
3614
3928
|
action: "updated",
|
|
@@ -3616,30 +3930,63 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3616
3930
|
};
|
|
3617
3931
|
}
|
|
3618
3932
|
function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
3619
|
-
const file =
|
|
3933
|
+
const file = join5(root, PENV_DIR2, GITIGNORE_FILE);
|
|
3620
3934
|
const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
|
|
3621
3935
|
const wanted = renderGitignore(decisions);
|
|
3622
|
-
const existing =
|
|
3936
|
+
const existing = existsSync4(file) ? readFileSync5(file, "utf8") : void 0;
|
|
3623
3937
|
if (existing === wanted) {
|
|
3624
3938
|
return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
|
|
3625
3939
|
}
|
|
3626
|
-
mkdirSync(
|
|
3627
|
-
|
|
3940
|
+
mkdirSync(join5(root, PENV_DIR2), { recursive: true });
|
|
3941
|
+
writeFileSync3(file, wanted, "utf8");
|
|
3628
3942
|
return {
|
|
3629
3943
|
target: "gitignore",
|
|
3630
3944
|
action: existing === void 0 ? "created" : "updated",
|
|
3631
3945
|
text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
|
|
3632
3946
|
};
|
|
3633
3947
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3948
|
+
var INJECT_MIN_VERSION = "0.6.0";
|
|
3949
|
+
function outdatedRuntimeWarning(root) {
|
|
3950
|
+
const version = installedPenvVersion(root);
|
|
3951
|
+
if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
|
|
3952
|
+
return void 0;
|
|
3953
|
+
}
|
|
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.`;
|
|
3955
|
+
}
|
|
3956
|
+
function installedPenvVersion(root) {
|
|
3957
|
+
const file = join5(root, "node_modules", "@penvhq", "penv", "package.json");
|
|
3958
|
+
if (!existsSync4(file)) {
|
|
3959
|
+
return void 0;
|
|
3960
|
+
}
|
|
3961
|
+
try {
|
|
3962
|
+
const version = JSON.parse(readFileSync5(file, "utf8")).version;
|
|
3963
|
+
return typeof version === "string" ? version : void 0;
|
|
3964
|
+
} catch {
|
|
3965
|
+
return void 0;
|
|
3966
|
+
}
|
|
3636
3967
|
}
|
|
3637
|
-
function
|
|
3968
|
+
function releaseTriple(version) {
|
|
3969
|
+
const parts = (version.split("-")[0] ?? "").split(".");
|
|
3970
|
+
const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
|
|
3971
|
+
return [at(0), at(1), at(2)];
|
|
3972
|
+
}
|
|
3973
|
+
function isBelow(a, b) {
|
|
3974
|
+
const [am, an, ap] = releaseTriple(a);
|
|
3975
|
+
const [bm, bn, bp] = releaseTriple(b);
|
|
3976
|
+
if (am !== bm) return am < bm;
|
|
3977
|
+
if (an !== bn) return an < bn;
|
|
3978
|
+
return ap < bp;
|
|
3979
|
+
}
|
|
3980
|
+
function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3638
3981
|
if (!decisions.inject) {
|
|
3639
3982
|
return void 0;
|
|
3640
3983
|
}
|
|
3641
|
-
const
|
|
3642
|
-
|
|
3984
|
+
const seam = seamFor(framework, {
|
|
3985
|
+
alias: decisions.alias,
|
|
3986
|
+
srcDir: srcPrefix(root),
|
|
3987
|
+
schemaFile: decisions.schemaFile
|
|
3988
|
+
});
|
|
3989
|
+
const outdated = outdatedRuntimeWarning(root);
|
|
3643
3990
|
if (seam.kind === "none") {
|
|
3644
3991
|
return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
|
|
3645
3992
|
}
|
|
@@ -3648,54 +3995,84 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3648
3995
|
target: "seam",
|
|
3649
3996
|
action: "info",
|
|
3650
3997
|
text: "Place the injection seam",
|
|
3651
|
-
note: seam.instruction
|
|
3998
|
+
note: withWarning(seam.instruction, outdated)
|
|
3652
3999
|
};
|
|
3653
4000
|
}
|
|
3654
|
-
const
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
|
|
4001
|
+
const alsoNote = writeAlso(root, seam.also);
|
|
4002
|
+
const file = join5(root, ...seam.file.split("/"));
|
|
4003
|
+
const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
|
|
4004
|
+
const notes = baseNotes.length === 0 ? "" : `
|
|
4005
|
+
${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
4006
|
+
if (existsSync4(file)) {
|
|
3658
4007
|
return {
|
|
3659
4008
|
target: "seam",
|
|
3660
4009
|
action: "info",
|
|
3661
4010
|
text: `Add the injection seam to ${seam.file}`,
|
|
3662
|
-
note: `${seam.ifPresent}${notes}
|
|
4011
|
+
note: withWarning(`${seam.ifPresent}${notes}`, outdated)
|
|
3663
4012
|
};
|
|
3664
4013
|
}
|
|
3665
4014
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3666
|
-
|
|
4015
|
+
writeFileSync3(file, seam.content, "utf8");
|
|
3667
4016
|
return {
|
|
3668
4017
|
target: "seam",
|
|
3669
|
-
action: "created",
|
|
4018
|
+
action: outdated === void 0 ? "created" : "info",
|
|
3670
4019
|
text: `Wrote ${seam.file} (runs the injection before your app)`,
|
|
3671
|
-
...notes === "" ? {} : { note: notes.trimStart() }
|
|
4020
|
+
...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
|
|
3672
4021
|
};
|
|
3673
4022
|
}
|
|
3674
|
-
function
|
|
4023
|
+
function writeAlso(root, also) {
|
|
4024
|
+
if (also === void 0) {
|
|
4025
|
+
return void 0;
|
|
4026
|
+
}
|
|
4027
|
+
const file = join5(root, ...also.file.split("/"));
|
|
4028
|
+
if (existsSync4(file)) {
|
|
4029
|
+
return also.ifPresent;
|
|
4030
|
+
}
|
|
4031
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
4032
|
+
writeFileSync3(file, also.content, "utf8");
|
|
4033
|
+
return `Wrote ${also.file} to register it.`;
|
|
4034
|
+
}
|
|
4035
|
+
function withWarning(note, warning) {
|
|
4036
|
+
if (warning === void 0) {
|
|
4037
|
+
return note;
|
|
4038
|
+
}
|
|
4039
|
+
return note === "" ? warning : `${note}
|
|
4040
|
+
\u26A0 ${warning}`;
|
|
4041
|
+
}
|
|
4042
|
+
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3675
4043
|
const steps = [
|
|
3676
4044
|
ensurePenvDir(root),
|
|
3677
|
-
|
|
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),
|
|
3678
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),
|
|
3679
4054
|
writeTsconfigAlias(root, decisions),
|
|
3680
4055
|
writeGitignore(root, decisions)
|
|
3681
4056
|
];
|
|
3682
|
-
const seam = writeSeam(root, decisions);
|
|
4057
|
+
const seam = writeSeam(root, decisions, framework);
|
|
3683
4058
|
return seam === void 0 ? steps : [...steps, seam];
|
|
3684
4059
|
}
|
|
3685
4060
|
function runInit(options) {
|
|
3686
4061
|
const root = resolve4(options.cwd);
|
|
3687
4062
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
3688
|
-
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
4063
|
+
return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
|
|
3689
4064
|
}
|
|
3690
4065
|
function renderInit(result2) {
|
|
3691
4066
|
const steps = result2.steps.map((step) => {
|
|
3692
4067
|
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3693
4068
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3694
4069
|
});
|
|
4070
|
+
const schemaStep = result2.steps.find((step) => step.target === "schema");
|
|
4071
|
+
const shapeFile = schemaStep?.action === "conflicted" ? result2.decisions.schemaFile : SCHEMA_SHAPE_FILE2;
|
|
3695
4072
|
return [
|
|
3696
4073
|
...formatSteps(steps),
|
|
3697
4074
|
"",
|
|
3698
|
-
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${
|
|
4075
|
+
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${shapeFile}, then:`,
|
|
3699
4076
|
tip(out.cyan("penv set <key>")),
|
|
3700
4077
|
...result2.decisions.environments.length === 0 ? [
|
|
3701
4078
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
@@ -3752,7 +4129,15 @@ var initCommand = defineCommand9({
|
|
|
3752
4129
|
if (!asked) {
|
|
3753
4130
|
write([...plan2.notes, ""]);
|
|
3754
4131
|
}
|
|
3755
|
-
write(
|
|
4132
|
+
write(
|
|
4133
|
+
renderInit(
|
|
4134
|
+
runInit({
|
|
4135
|
+
cwd: root,
|
|
4136
|
+
decisions,
|
|
4137
|
+
...plan2.detected && { framework: plan2.detected.name }
|
|
4138
|
+
})
|
|
4139
|
+
)
|
|
4140
|
+
);
|
|
3756
4141
|
});
|
|
3757
4142
|
}
|
|
3758
4143
|
});
|
|
@@ -3925,7 +4310,7 @@ function decisionsOf(config, cwd) {
|
|
|
3925
4310
|
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
3926
4311
|
// choice, so it is not turned on here.
|
|
3927
4312
|
inject: false,
|
|
3928
|
-
schemaFile:
|
|
4313
|
+
schemaFile: schemaFileOf5(config),
|
|
3929
4314
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3930
4315
|
alias: detectAlias(cwd)
|
|
3931
4316
|
};
|
|
@@ -3947,14 +4332,14 @@ function configInEffect(cwd, environment) {
|
|
|
3947
4332
|
function importDotenv(options) {
|
|
3948
4333
|
const cwd = resolve5(options.cwd);
|
|
3949
4334
|
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
3950
|
-
if (!
|
|
4335
|
+
if (!existsSync5(file)) {
|
|
3951
4336
|
throw new PenvError13(
|
|
3952
4337
|
"IMPORT_FILE_MISSING",
|
|
3953
4338
|
`There is no file at ${file} to import`,
|
|
3954
4339
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
3955
4340
|
);
|
|
3956
4341
|
}
|
|
3957
|
-
const parsed = parseDotenv(
|
|
4342
|
+
const parsed = parseDotenv(readFileSync6(file, "utf8"));
|
|
3958
4343
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
3959
4344
|
const source = displayPath2(cwd, file);
|
|
3960
4345
|
const named = scopeFromFilename(file, config);
|
|
@@ -3992,6 +4377,7 @@ function importDotenv(options) {
|
|
|
3992
4377
|
}
|
|
3993
4378
|
const backup = `${file}${BACKUP_SUFFIX}`;
|
|
3994
4379
|
copyFileSync(file, backup);
|
|
4380
|
+
refreshSnapshot(project);
|
|
3995
4381
|
return {
|
|
3996
4382
|
root: project.root,
|
|
3997
4383
|
file,
|
|
@@ -4304,7 +4690,7 @@ var listCommand = defineCommand12({
|
|
|
4304
4690
|
import {
|
|
4305
4691
|
accessPath as accessPath5,
|
|
4306
4692
|
formatMetaFile,
|
|
4307
|
-
formatValueFile as
|
|
4693
|
+
formatValueFile as formatValueFile6,
|
|
4308
4694
|
openValue as openValue4,
|
|
4309
4695
|
PenvError as PenvError15,
|
|
4310
4696
|
parameterId as parameterId5,
|
|
@@ -4327,7 +4713,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
4327
4713
|
if (environment === void 0) {
|
|
4328
4714
|
throw new PenvError15(
|
|
4329
4715
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
4330
|
-
`${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`,
|
|
4331
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."
|
|
4332
4718
|
);
|
|
4333
4719
|
}
|
|
@@ -4336,7 +4722,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
4336
4722
|
if (opened.kind === "failed") {
|
|
4337
4723
|
throw new PenvError15(
|
|
4338
4724
|
"VALUE_UNDECRYPTABLE",
|
|
4339
|
-
`${PENV_DIR}/${
|
|
4725
|
+
`${PENV_DIR}/${formatValueFile6(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
4340
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."
|
|
4341
4727
|
);
|
|
4342
4728
|
}
|
|
@@ -4401,19 +4787,21 @@ async function runMove(options) {
|
|
|
4401
4787
|
if (meta !== void 0) {
|
|
4402
4788
|
await project.provider.removeMeta(from);
|
|
4403
4789
|
}
|
|
4790
|
+
refreshSnapshot(project);
|
|
4404
4791
|
return {
|
|
4405
4792
|
from: parameterId5(from),
|
|
4406
4793
|
to: parameterId5(to),
|
|
4407
4794
|
files: planned.map((file) => ({
|
|
4408
|
-
from:
|
|
4409
|
-
to:
|
|
4795
|
+
from: formatValueFile6(file.source),
|
|
4796
|
+
to: formatValueFile6(file.target),
|
|
4410
4797
|
resealed: file.resealed
|
|
4411
4798
|
})),
|
|
4412
4799
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
4413
4800
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
4414
4801
|
// that printed the first while moving the second names no file on disk.
|
|
4415
4802
|
meta: meta === void 0 ? void 0 : formatMetaFile({ ...to, format: "json" }),
|
|
4416
|
-
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
|
|
4803
|
+
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") },
|
|
4804
|
+
schemaFile: schemaShapeFileOf(project)
|
|
4417
4805
|
};
|
|
4418
4806
|
}
|
|
4419
4807
|
function renderMove(result2) {
|
|
@@ -4430,7 +4818,7 @@ function renderMove(result2) {
|
|
|
4430
4818
|
lines.push(
|
|
4431
4819
|
"",
|
|
4432
4820
|
tip(
|
|
4433
|
-
|
|
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.`
|
|
4434
4822
|
)
|
|
4435
4823
|
);
|
|
4436
4824
|
return lines;
|
|
@@ -4517,6 +4905,7 @@ async function runPull(options) {
|
|
|
4517
4905
|
tree.writeMetaSync(ref, block);
|
|
4518
4906
|
meta += 1;
|
|
4519
4907
|
}
|
|
4908
|
+
refreshSnapshot(project);
|
|
4520
4909
|
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
4521
4910
|
}
|
|
4522
4911
|
function renderPull(result2) {
|
|
@@ -4585,7 +4974,7 @@ var pullCommand = defineCommand14({
|
|
|
4585
4974
|
});
|
|
4586
4975
|
|
|
4587
4976
|
// src/commands/remove.ts
|
|
4588
|
-
import { formatValueFile as
|
|
4977
|
+
import { formatValueFile as formatValueFile7 } from "@penvhq/core";
|
|
4589
4978
|
import { defineCommand as defineCommand15 } from "citty";
|
|
4590
4979
|
async function runRemove(options) {
|
|
4591
4980
|
const project = openProject(options.cwd);
|
|
@@ -4603,12 +4992,13 @@ async function runRemove(options) {
|
|
|
4603
4992
|
continue;
|
|
4604
4993
|
}
|
|
4605
4994
|
await project.provider.remove(file);
|
|
4606
|
-
removed.push(
|
|
4995
|
+
removed.push(formatValueFile7(file));
|
|
4607
4996
|
}
|
|
4997
|
+
refreshSnapshot(project);
|
|
4608
4998
|
return {
|
|
4609
4999
|
parameter: options.key,
|
|
4610
5000
|
removed,
|
|
4611
|
-
considered: files.map((file) =>
|
|
5001
|
+
considered: files.map((file) => formatValueFile7(file))
|
|
4612
5002
|
};
|
|
4613
5003
|
}
|
|
4614
5004
|
function renderRemove(result2) {
|
|
@@ -4684,6 +5074,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
4684
5074
|
value,
|
|
4685
5075
|
environment
|
|
4686
5076
|
});
|
|
5077
|
+
refreshSnapshot(project);
|
|
4687
5078
|
return;
|
|
4688
5079
|
}
|
|
4689
5080
|
await provider.write(rotatingFile(ref, environment), value);
|
|
@@ -4851,11 +5242,53 @@ var rotateCommand = defineCommand16({
|
|
|
4851
5242
|
}
|
|
4852
5243
|
});
|
|
4853
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
|
+
|
|
4854
5287
|
// src/commands/watch.ts
|
|
4855
|
-
import { existsSync as
|
|
5288
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
4856
5289
|
import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
|
|
4857
|
-
import { schemaFileOf as
|
|
4858
|
-
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";
|
|
4859
5292
|
var DEBOUNCE_MS = 100;
|
|
4860
5293
|
function runWatch(options) {
|
|
4861
5294
|
const project = openProject(options.cwd);
|
|
@@ -4920,14 +5353,14 @@ function runWatch(options) {
|
|
|
4920
5353
|
if (closed || recovery === void 0) {
|
|
4921
5354
|
return;
|
|
4922
5355
|
}
|
|
4923
|
-
if (!
|
|
5356
|
+
if (!existsSync6(parent)) {
|
|
4924
5357
|
stop(recovery);
|
|
4925
5358
|
return;
|
|
4926
5359
|
}
|
|
4927
5360
|
if (filename !== null && basename2(filename) !== name) {
|
|
4928
5361
|
return;
|
|
4929
5362
|
}
|
|
4930
|
-
if (!
|
|
5363
|
+
if (!existsSync6(target)) {
|
|
4931
5364
|
return;
|
|
4932
5365
|
}
|
|
4933
5366
|
stop(recovery);
|
|
@@ -4951,7 +5384,7 @@ function runWatch(options) {
|
|
|
4951
5384
|
if (closed) {
|
|
4952
5385
|
return;
|
|
4953
5386
|
}
|
|
4954
|
-
if (!
|
|
5387
|
+
if (!existsSync6(target)) {
|
|
4955
5388
|
if (watcher !== void 0) {
|
|
4956
5389
|
stop(watcher);
|
|
4957
5390
|
}
|
|
@@ -4987,8 +5420,9 @@ function runWatch(options) {
|
|
|
4987
5420
|
}
|
|
4988
5421
|
addWatcher(project.penvDir, true);
|
|
4989
5422
|
addWatcher(dirname4(project.configFile), false, configFile);
|
|
5423
|
+
addWatcher(project.root, false, SCHEMA_SHAPE_FILE3);
|
|
4990
5424
|
if (schemaInsideTree2(project.config) === void 0) {
|
|
4991
|
-
const schemaFile = resolve6(project.root,
|
|
5425
|
+
const schemaFile = resolve6(project.root, schemaFileOf6(project.config));
|
|
4992
5426
|
addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
|
|
4993
5427
|
}
|
|
4994
5428
|
void validate();
|
|
@@ -5032,7 +5466,7 @@ function renderDrift(drift, environment) {
|
|
|
5032
5466
|
}
|
|
5033
5467
|
return lines;
|
|
5034
5468
|
}
|
|
5035
|
-
var watchCommand =
|
|
5469
|
+
var watchCommand = defineCommand18({
|
|
5036
5470
|
meta: {
|
|
5037
5471
|
name: "watch",
|
|
5038
5472
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -5071,7 +5505,7 @@ var watchCommand = defineCommand17({
|
|
|
5071
5505
|
});
|
|
5072
5506
|
|
|
5073
5507
|
// src/index.ts
|
|
5074
|
-
var main =
|
|
5508
|
+
var main = defineCommand19({
|
|
5075
5509
|
meta: {
|
|
5076
5510
|
name: "penv",
|
|
5077
5511
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -5092,6 +5526,7 @@ var main = defineCommand18({
|
|
|
5092
5526
|
encrypt: encryptCommand,
|
|
5093
5527
|
decrypt: decryptCommand,
|
|
5094
5528
|
key: keyCommand,
|
|
5529
|
+
snapshot: snapshotCommand,
|
|
5095
5530
|
validate: validateCommand,
|
|
5096
5531
|
doctor: doctorCommand,
|
|
5097
5532
|
watch: watchCommand
|
|
@@ -5113,6 +5548,7 @@ export {
|
|
|
5113
5548
|
renderPull,
|
|
5114
5549
|
renderPush,
|
|
5115
5550
|
renderRotate,
|
|
5551
|
+
renderSnapshot,
|
|
5116
5552
|
renderWatch,
|
|
5117
5553
|
runDecrypt,
|
|
5118
5554
|
runDoctor,
|
|
@@ -5130,6 +5566,7 @@ export {
|
|
|
5130
5566
|
runRemove,
|
|
5131
5567
|
runRotate,
|
|
5132
5568
|
runSet,
|
|
5569
|
+
runSnapshot,
|
|
5133
5570
|
runValidate,
|
|
5134
5571
|
runWatch
|
|
5135
5572
|
};
|