@penvhq/cli 0.1.0 → 0.2.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 +785 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -7
- package/dist/index.d.ts +95 -7
- package/dist/index.js +796 -154
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
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 defineCommand17 } from "citty";
|
|
4
4
|
|
|
5
5
|
// src/commands/doctor.ts
|
|
6
6
|
import {
|
|
7
7
|
accessPath as accessPath3,
|
|
8
|
+
assertNever,
|
|
8
9
|
effectiveMeta,
|
|
10
|
+
formatValueFile as formatValueFile2,
|
|
9
11
|
isPublicVariable,
|
|
10
12
|
isRequired,
|
|
11
13
|
isSecret,
|
|
14
|
+
isStuck,
|
|
15
|
+
openValue as openValue2,
|
|
12
16
|
resolveAll as resolveAll2,
|
|
17
|
+
rotationOf,
|
|
18
|
+
tryParseDuration,
|
|
13
19
|
variableName as variableName3
|
|
14
20
|
} from "@penvhq/core";
|
|
15
21
|
import { createGithubSink as createGithubSink2 } from "@penvhq/sink-github";
|
|
16
22
|
import { defineCommand as defineCommand3 } from "citty";
|
|
17
23
|
|
|
18
24
|
// src/project.ts
|
|
19
|
-
import { dirname, resolve } from "path";
|
|
25
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
20
26
|
import {
|
|
21
27
|
candidatesFor,
|
|
22
28
|
formatValueFile,
|
|
@@ -32,12 +38,30 @@ import {
|
|
|
32
38
|
import { FilesystemProvider } from "@penvhq/provider-filesystem";
|
|
33
39
|
|
|
34
40
|
// src/registry.ts
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
import { dirname, resolve } from "path";
|
|
43
|
+
import { pathToFileURL } from "url";
|
|
35
44
|
import { PenvError } from "@penvhq/core";
|
|
36
45
|
import { createFilesystemProvider } from "@penvhq/provider-filesystem";
|
|
46
|
+
import { createMockProvider } from "@penvhq/provider-mock";
|
|
47
|
+
import { createVaultProvider } from "@penvhq/provider-vault";
|
|
48
|
+
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
37
49
|
var LOCAL_TREE_TYPE = "filesystem";
|
|
38
50
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
39
|
-
[LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })]
|
|
51
|
+
[LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
|
|
52
|
+
["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
|
|
53
|
+
["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
|
|
40
54
|
]);
|
|
55
|
+
var CONTRACT_METHODS = [
|
|
56
|
+
"read",
|
|
57
|
+
"write",
|
|
58
|
+
"list",
|
|
59
|
+
"remove",
|
|
60
|
+
"readMeta",
|
|
61
|
+
"writeMeta",
|
|
62
|
+
"removeMeta"
|
|
63
|
+
];
|
|
64
|
+
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
41
65
|
function isProviderRegistered(type) {
|
|
42
66
|
return REGISTRY.has(type);
|
|
43
67
|
}
|
|
@@ -48,20 +72,94 @@ function createProvider(type, context) {
|
|
|
48
72
|
}
|
|
49
73
|
return factory(context);
|
|
50
74
|
}
|
|
51
|
-
function
|
|
75
|
+
async function createSourceProvider(type, context) {
|
|
76
|
+
if (REGISTRY.has(type)) {
|
|
77
|
+
return createProvider(type, context);
|
|
78
|
+
}
|
|
79
|
+
return loadPluginProvider(type, context);
|
|
80
|
+
}
|
|
81
|
+
async function loadPluginProvider(type, context) {
|
|
82
|
+
const fromDir = resolutionBase(context);
|
|
83
|
+
const specifier = pluginSpecifier(context.providerConfig, type);
|
|
84
|
+
const resolved = resolvePlugin(specifier, fromDir);
|
|
85
|
+
if (resolved === void 0) {
|
|
86
|
+
throw unknownProvider(type, context.environment, specifier);
|
|
87
|
+
}
|
|
88
|
+
let mod;
|
|
89
|
+
try {
|
|
90
|
+
mod = await importPlugin(resolved);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new PenvError(
|
|
93
|
+
"PROVIDER_PLUGIN_LOAD",
|
|
94
|
+
`The provider package \`${specifier}\` for type \`${type}\` failed to load`,
|
|
95
|
+
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
99
|
+
if (typeof factory !== "function") {
|
|
100
|
+
throw new PenvError(
|
|
101
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
102
|
+
`\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
103
|
+
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const provider = await factory(context);
|
|
107
|
+
assertSatisfiesContract(provider, specifier);
|
|
108
|
+
return provider;
|
|
109
|
+
}
|
|
110
|
+
function assertProvidersRegistered(config, projectRoot) {
|
|
52
111
|
for (const [environment, provider] of Object.entries(config.providers)) {
|
|
53
|
-
if (
|
|
54
|
-
|
|
112
|
+
if (isProviderRegistered(provider.type)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const specifier = pluginSpecifier(provider, provider.type);
|
|
116
|
+
if (resolvePlugin(specifier, projectRoot) === void 0) {
|
|
117
|
+
throw unknownProvider(provider.type, environment, specifier);
|
|
55
118
|
}
|
|
56
119
|
}
|
|
57
120
|
}
|
|
58
|
-
function
|
|
121
|
+
function pluginSpecifier(providerConfig, type) {
|
|
122
|
+
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
123
|
+
}
|
|
124
|
+
function resolutionBase(context) {
|
|
125
|
+
return dirname(context.root);
|
|
126
|
+
}
|
|
127
|
+
function resolvePlugin(specifier, fromDir) {
|
|
128
|
+
try {
|
|
129
|
+
const require2 = createRequire(resolve(fromDir, "noop.js"));
|
|
130
|
+
return require2.resolve(specifier);
|
|
131
|
+
} catch {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function importPlugin(resolvedPath) {
|
|
136
|
+
const cached2 = pluginModuleCache.get(resolvedPath);
|
|
137
|
+
if (cached2 !== void 0) {
|
|
138
|
+
return cached2;
|
|
139
|
+
}
|
|
140
|
+
const loading = import(pathToFileURL(resolvedPath).href);
|
|
141
|
+
pluginModuleCache.set(resolvedPath, loading);
|
|
142
|
+
return loading;
|
|
143
|
+
}
|
|
144
|
+
function assertSatisfiesContract(provider, specifier) {
|
|
145
|
+
for (const method of CONTRACT_METHODS) {
|
|
146
|
+
if (typeof provider[method] !== "function") {
|
|
147
|
+
throw new PenvError(
|
|
148
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
149
|
+
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
150
|
+
"It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function unknownProvider(type, environment, specifier) {
|
|
59
156
|
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
60
157
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
158
|
+
const remedy = specifier === void 0 ? `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.` : `Install its package with \`npm i ${specifier}\`, or name a built-in provider: ${known}.`;
|
|
61
159
|
return new PenvError(
|
|
62
160
|
"UNKNOWN_PROVIDER",
|
|
63
161
|
`The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
|
|
64
|
-
|
|
162
|
+
remedy
|
|
65
163
|
);
|
|
66
164
|
}
|
|
67
165
|
|
|
@@ -69,9 +167,9 @@ function unknownProvider(type, environment) {
|
|
|
69
167
|
var PENV_DIR = ".penv";
|
|
70
168
|
function openProject(cwd) {
|
|
71
169
|
const { config, file } = loadConfig(cwd);
|
|
72
|
-
const root =
|
|
73
|
-
const penvDir =
|
|
74
|
-
assertProvidersRegistered(config);
|
|
170
|
+
const root = dirname2(file);
|
|
171
|
+
const penvDir = resolve2(root, PENV_DIR);
|
|
172
|
+
assertProvidersRegistered(config, root);
|
|
75
173
|
return {
|
|
76
174
|
root,
|
|
77
175
|
configFile: file,
|
|
@@ -90,6 +188,18 @@ function localTree(project) {
|
|
|
90
188
|
}
|
|
91
189
|
return project.provider;
|
|
92
190
|
}
|
|
191
|
+
async function sourceProviderFor(project, environment) {
|
|
192
|
+
const providerConfig = project.config.providers[environment];
|
|
193
|
+
if (providerConfig === void 0) {
|
|
194
|
+
return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
|
|
195
|
+
}
|
|
196
|
+
return createSourceProvider(providerConfig.type, {
|
|
197
|
+
root: project.penvDir,
|
|
198
|
+
config: project.config,
|
|
199
|
+
providerConfig,
|
|
200
|
+
environment
|
|
201
|
+
});
|
|
202
|
+
}
|
|
93
203
|
function targetEnvironment(project, explicit) {
|
|
94
204
|
return resolveEnvironment(project.config, explicit);
|
|
95
205
|
}
|
|
@@ -471,36 +581,36 @@ async function runPush(options) {
|
|
|
471
581
|
decrypted: outbound.filter((item) => item.encrypted).length
|
|
472
582
|
};
|
|
473
583
|
}
|
|
474
|
-
function renderPush(
|
|
475
|
-
if (
|
|
584
|
+
function renderPush(result2) {
|
|
585
|
+
if (result2.pushed === 0) {
|
|
476
586
|
return formatRows([
|
|
477
587
|
{
|
|
478
588
|
glyph: CHECK,
|
|
479
589
|
label: "Nothing to push",
|
|
480
|
-
subject: `no values resolve for environment ${
|
|
590
|
+
subject: `no values resolve for environment ${result2.environment}`
|
|
481
591
|
}
|
|
482
592
|
]);
|
|
483
593
|
}
|
|
484
|
-
const target = `GitHub Actions for environment ${
|
|
594
|
+
const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
|
|
485
595
|
const rows = [
|
|
486
596
|
{
|
|
487
597
|
glyph: CHECK,
|
|
488
598
|
label: "Pushed",
|
|
489
|
-
subject: `${
|
|
599
|
+
subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
|
|
490
600
|
detail: `to ${target}`
|
|
491
601
|
},
|
|
492
602
|
{
|
|
493
603
|
glyph: CHECK,
|
|
494
604
|
label: "Scopes",
|
|
495
|
-
subject: `${
|
|
605
|
+
subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
|
|
496
606
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
497
607
|
}
|
|
498
608
|
];
|
|
499
|
-
if (
|
|
609
|
+
if (result2.decrypted > 0) {
|
|
500
610
|
rows.push({
|
|
501
611
|
glyph: WARN,
|
|
502
612
|
label: "Decrypted",
|
|
503
|
-
subject: `${
|
|
613
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
504
614
|
detail: "sent as plaintext for GitHub to re-seal under its own key"
|
|
505
615
|
});
|
|
506
616
|
}
|
|
@@ -520,19 +630,19 @@ var pushCommand = defineCommand({
|
|
|
520
630
|
},
|
|
521
631
|
run({ args }) {
|
|
522
632
|
return guard(async () => {
|
|
523
|
-
const
|
|
633
|
+
const result2 = await runPush({
|
|
524
634
|
cwd: process.cwd(),
|
|
525
635
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
526
636
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
527
637
|
});
|
|
528
|
-
write(renderPush(
|
|
638
|
+
write(renderPush(result2));
|
|
529
639
|
});
|
|
530
640
|
}
|
|
531
641
|
});
|
|
532
642
|
|
|
533
643
|
// src/commands/validate.ts
|
|
534
644
|
import { resolve as resolvePath } from "path";
|
|
535
|
-
import { pathToFileURL } from "url";
|
|
645
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
536
646
|
import {
|
|
537
647
|
accessPath as accessPath2,
|
|
538
648
|
checkNameCollisions as checkNameCollisions2,
|
|
@@ -610,16 +720,16 @@ function validationIssuesOf(cause, schemaPath) {
|
|
|
610
720
|
}
|
|
611
721
|
var schemaLoads = Promise.resolve();
|
|
612
722
|
function exclusively(work) {
|
|
613
|
-
const
|
|
614
|
-
schemaLoads =
|
|
723
|
+
const result2 = schemaLoads.then(work, work);
|
|
724
|
+
schemaLoads = result2.then(
|
|
615
725
|
() => void 0,
|
|
616
726
|
() => void 0
|
|
617
727
|
);
|
|
618
|
-
return
|
|
728
|
+
return result2;
|
|
619
729
|
}
|
|
620
730
|
function loadSchema(project, environment) {
|
|
621
731
|
const schemaPath = schemaFileOf(project.config);
|
|
622
|
-
const file =
|
|
732
|
+
const file = pathToFileURL2(resolvePath(project.root, schemaPath)).href;
|
|
623
733
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
624
734
|
}
|
|
625
735
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
@@ -721,9 +831,9 @@ async function runValidate(options) {
|
|
|
721
831
|
}
|
|
722
832
|
}
|
|
723
833
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
724
|
-
const
|
|
725
|
-
if (!
|
|
726
|
-
for (const problem of
|
|
834
|
+
const result2 = schema.safeParse(object);
|
|
835
|
+
if (!result2.success) {
|
|
836
|
+
for (const problem of result2.error.issues) {
|
|
727
837
|
const path = problem.path.join(".");
|
|
728
838
|
if (undecryptable.has(path)) {
|
|
729
839
|
continue;
|
|
@@ -739,24 +849,24 @@ async function runValidate(options) {
|
|
|
739
849
|
}
|
|
740
850
|
return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
|
|
741
851
|
}
|
|
742
|
-
function renderValidate(
|
|
743
|
-
if (
|
|
852
|
+
function renderValidate(result2) {
|
|
853
|
+
if (result2.ok) {
|
|
744
854
|
return formatRows([
|
|
745
855
|
{
|
|
746
856
|
glyph: CHECK,
|
|
747
857
|
label: "Schema valid",
|
|
748
|
-
subject: `${
|
|
858
|
+
subject: `${result2.parameters} parameters for environment ${result2.environment}`
|
|
749
859
|
}
|
|
750
860
|
]);
|
|
751
861
|
}
|
|
752
|
-
const rows =
|
|
862
|
+
const rows = result2.issues.map((issue) => ({
|
|
753
863
|
glyph: WARN,
|
|
754
864
|
label: LABELS[issue.kind],
|
|
755
865
|
subject: issue.subject,
|
|
756
866
|
detail: issue.message
|
|
757
867
|
}));
|
|
758
868
|
const lines = formatRows(rows);
|
|
759
|
-
const remedies = [...new Set(
|
|
869
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
|
|
760
870
|
for (const remedy of remedies) {
|
|
761
871
|
if (remedy !== void 0) {
|
|
762
872
|
lines.push(` ${remedy}`);
|
|
@@ -774,12 +884,12 @@ var validateCommand = defineCommand2({
|
|
|
774
884
|
},
|
|
775
885
|
run({ args }) {
|
|
776
886
|
return guard(async () => {
|
|
777
|
-
const
|
|
887
|
+
const result2 = await runValidate({
|
|
778
888
|
cwd: process.cwd(),
|
|
779
889
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
780
890
|
});
|
|
781
|
-
write(renderValidate(
|
|
782
|
-
if (!
|
|
891
|
+
write(renderValidate(result2));
|
|
892
|
+
if (!result2.ok) {
|
|
783
893
|
process.exitCode = 1;
|
|
784
894
|
}
|
|
785
895
|
});
|
|
@@ -787,6 +897,7 @@ var validateCommand = defineCommand2({
|
|
|
787
897
|
});
|
|
788
898
|
|
|
789
899
|
// src/commands/doctor.ts
|
|
900
|
+
var STUCK_THRESHOLD_MS = 864e5;
|
|
790
901
|
function skipped(check, label) {
|
|
791
902
|
return {
|
|
792
903
|
check,
|
|
@@ -800,6 +911,8 @@ async function runDoctor(options) {
|
|
|
800
911
|
const project = openProject(options.cwd);
|
|
801
912
|
const environment = targetEnvironment(project, options.environment);
|
|
802
913
|
const findings = [];
|
|
914
|
+
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
915
|
+
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
803
916
|
const { schema, issues } = await loadSchema(project, environment);
|
|
804
917
|
if (schema !== void 0) {
|
|
805
918
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -848,6 +961,14 @@ async function runDoctor(options) {
|
|
|
848
961
|
findings.push(...plaintextSecretFindings(subjects, environment));
|
|
849
962
|
findings.push(...publicSecretFindings(subjects, environment, project.config));
|
|
850
963
|
findings.push(...encryptionFindings(subjects, environment));
|
|
964
|
+
const rotation = await rotationSubjects(project, environment, subjects, options.source);
|
|
965
|
+
if (rotation.kind === "unreachable") {
|
|
966
|
+
findings.push(rotation.finding);
|
|
967
|
+
} else {
|
|
968
|
+
findings.push(...overdueFindings(rotation.subjects, environment, now));
|
|
969
|
+
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
970
|
+
}
|
|
971
|
+
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
851
972
|
findings.push(...await sinkFindings(project, environment, options.sink));
|
|
852
973
|
findings.push({
|
|
853
974
|
check: "provider",
|
|
@@ -1265,6 +1386,246 @@ async function sinkFindings(project, environment, override) {
|
|
|
1265
1386
|
});
|
|
1266
1387
|
return findings;
|
|
1267
1388
|
}
|
|
1389
|
+
function humanizeMs(ms) {
|
|
1390
|
+
const abs = Math.max(0, ms);
|
|
1391
|
+
const day = 864e5;
|
|
1392
|
+
const hour = 36e5;
|
|
1393
|
+
const minute = 6e4;
|
|
1394
|
+
const round = (value, unit) => {
|
|
1395
|
+
const n = Math.round(value);
|
|
1396
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
1397
|
+
};
|
|
1398
|
+
if (abs >= day) return round(abs / day, "day");
|
|
1399
|
+
if (abs >= hour) return round(abs / hour, "hour");
|
|
1400
|
+
if (abs >= minute) return round(abs / minute, "minute");
|
|
1401
|
+
return round(abs / 1e3, "second");
|
|
1402
|
+
}
|
|
1403
|
+
function refPathOf(ref) {
|
|
1404
|
+
return [...ref.namespace, ref.name].join("/");
|
|
1405
|
+
}
|
|
1406
|
+
async function rotationSubjects(project, environment, local, override) {
|
|
1407
|
+
const providerConfig = project.config.providers[environment];
|
|
1408
|
+
if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
|
|
1409
|
+
return { kind: "read", subjects: local };
|
|
1410
|
+
}
|
|
1411
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1412
|
+
try {
|
|
1413
|
+
const subjects = await Promise.all(
|
|
1414
|
+
local.map(async ({ resolution }) => ({
|
|
1415
|
+
resolution,
|
|
1416
|
+
meta: await source.readMeta(resolution.ref)
|
|
1417
|
+
}))
|
|
1418
|
+
);
|
|
1419
|
+
return { kind: "read", subjects };
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
return {
|
|
1422
|
+
kind: "unreachable",
|
|
1423
|
+
finding: {
|
|
1424
|
+
check: "rotation-overdue",
|
|
1425
|
+
severity: "unknown",
|
|
1426
|
+
label: "Rotation",
|
|
1427
|
+
subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
|
|
1428
|
+
detail: errorDetail(error)
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function overdueFindings(subjects, environment, now) {
|
|
1434
|
+
const findings = [];
|
|
1435
|
+
for (const { resolution, meta } of subjects) {
|
|
1436
|
+
const { policy, lastRotated } = rotationOf(meta, environment);
|
|
1437
|
+
if (policy === void 0 || lastRotated === null) {
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const interval = tryParseDuration(policy);
|
|
1441
|
+
if (interval === void 0) {
|
|
1442
|
+
findings.push({
|
|
1443
|
+
check: "rotation-overdue",
|
|
1444
|
+
severity: "warning",
|
|
1445
|
+
label: "Rotation policy invalid",
|
|
1446
|
+
subject: resolution.parameter,
|
|
1447
|
+
detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
|
|
1448
|
+
});
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const last = Date.parse(lastRotated);
|
|
1452
|
+
if (Number.isNaN(last)) {
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
const overdueBy = now.getTime() - last - interval;
|
|
1456
|
+
if (overdueBy <= 0) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
findings.push({
|
|
1460
|
+
check: "rotation-overdue",
|
|
1461
|
+
severity: "warning",
|
|
1462
|
+
label: "Rotation overdue",
|
|
1463
|
+
subject: resolution.parameter,
|
|
1464
|
+
detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
|
|
1465
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
if (findings.length > 0) {
|
|
1469
|
+
return findings;
|
|
1470
|
+
}
|
|
1471
|
+
return [
|
|
1472
|
+
{
|
|
1473
|
+
check: "rotation-overdue",
|
|
1474
|
+
severity: "pass",
|
|
1475
|
+
label: "Rotation freshness",
|
|
1476
|
+
subject: `no parameter is past its rotation policy for ${environment}`
|
|
1477
|
+
}
|
|
1478
|
+
];
|
|
1479
|
+
}
|
|
1480
|
+
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1481
|
+
const findings = [];
|
|
1482
|
+
for (const { resolution, meta } of subjects) {
|
|
1483
|
+
if (!isStuck(meta, environment, now, stuckThresholdMs)) {
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const { rotatingSince } = rotationOf(meta, environment);
|
|
1487
|
+
if (rotatingSince === null) {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const openFor = now.getTime() - Date.parse(rotatingSince);
|
|
1491
|
+
findings.push({
|
|
1492
|
+
check: "rotation-stuck",
|
|
1493
|
+
severity: "warning",
|
|
1494
|
+
label: "Rotation stuck",
|
|
1495
|
+
subject: resolution.parameter,
|
|
1496
|
+
detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
|
|
1497
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
if (findings.length > 0) {
|
|
1501
|
+
return findings;
|
|
1502
|
+
}
|
|
1503
|
+
return [
|
|
1504
|
+
{
|
|
1505
|
+
check: "rotation-stuck",
|
|
1506
|
+
severity: "pass",
|
|
1507
|
+
label: "Rotation progress",
|
|
1508
|
+
subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
|
|
1509
|
+
}
|
|
1510
|
+
];
|
|
1511
|
+
}
|
|
1512
|
+
function driftKey(file) {
|
|
1513
|
+
return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
|
|
1514
|
+
}
|
|
1515
|
+
function scopeKey(scope) {
|
|
1516
|
+
switch (scope.kind) {
|
|
1517
|
+
case "unscoped":
|
|
1518
|
+
return "unscoped";
|
|
1519
|
+
case "environment":
|
|
1520
|
+
return `environment:${scope.environment}`;
|
|
1521
|
+
case "local":
|
|
1522
|
+
return "local";
|
|
1523
|
+
case "environment-local":
|
|
1524
|
+
return `environment-local:${scope.environment}`;
|
|
1525
|
+
default:
|
|
1526
|
+
return assertNever(scope, "scope");
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function relevantToEnvironment(file, environment) {
|
|
1530
|
+
const scope = file.scope;
|
|
1531
|
+
if (scope.kind === "unscoped") return true;
|
|
1532
|
+
if (scope.kind === "environment") return scope.environment === environment;
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
async function readRelevant(provider, environment) {
|
|
1536
|
+
const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
|
|
1537
|
+
const stored = await Promise.all(files.map((file) => provider.read(file)));
|
|
1538
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1539
|
+
for (const [index, file] of files.entries()) {
|
|
1540
|
+
const value = stored[index];
|
|
1541
|
+
if (value !== void 0) {
|
|
1542
|
+
entries.set(driftKey(file), { file, stored: value });
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return entries;
|
|
1546
|
+
}
|
|
1547
|
+
async function providerDriftFindings(project, environment, override) {
|
|
1548
|
+
const providerConfig = project.config.providers[environment];
|
|
1549
|
+
if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
|
|
1550
|
+
return [
|
|
1551
|
+
{
|
|
1552
|
+
check: "provider-value-drift",
|
|
1553
|
+
severity: "pass",
|
|
1554
|
+
label: "Provider values",
|
|
1555
|
+
subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
|
|
1556
|
+
}
|
|
1557
|
+
];
|
|
1558
|
+
}
|
|
1559
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1560
|
+
let local;
|
|
1561
|
+
let remote;
|
|
1562
|
+
try {
|
|
1563
|
+
local = await readRelevant(project.provider, environment);
|
|
1564
|
+
remote = await readRelevant(source, environment);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
return [
|
|
1567
|
+
{
|
|
1568
|
+
check: "provider-value-drift",
|
|
1569
|
+
severity: "unknown",
|
|
1570
|
+
label: "Provider values",
|
|
1571
|
+
subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
|
|
1572
|
+
detail: errorDetail(error)
|
|
1573
|
+
}
|
|
1574
|
+
];
|
|
1575
|
+
}
|
|
1576
|
+
const keys = keySourceFor(project, environment);
|
|
1577
|
+
const findings = [];
|
|
1578
|
+
const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
|
|
1579
|
+
for (const address of addresses) {
|
|
1580
|
+
const here = local.get(address);
|
|
1581
|
+
const there = remote.get(address);
|
|
1582
|
+
if (here !== void 0 && there !== void 0) {
|
|
1583
|
+
const opened = openValue2(here.file, here.stored, keys);
|
|
1584
|
+
if (opened.kind !== "plaintext") {
|
|
1585
|
+
findings.push({
|
|
1586
|
+
check: "provider-value-drift",
|
|
1587
|
+
severity: "unknown",
|
|
1588
|
+
label: "Provider value unreadable",
|
|
1589
|
+
subject: formatValueFile2(here.file),
|
|
1590
|
+
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1591
|
+
});
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (opened.value !== there.stored) {
|
|
1595
|
+
findings.push({
|
|
1596
|
+
check: "provider-value-drift",
|
|
1597
|
+
severity: "failure",
|
|
1598
|
+
label: "Provider value drift",
|
|
1599
|
+
subject: formatValueFile2(here.file),
|
|
1600
|
+
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const present = here ?? there;
|
|
1606
|
+
if (present === void 0) {
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
findings.push({
|
|
1610
|
+
check: "provider-value-drift",
|
|
1611
|
+
severity: "warning",
|
|
1612
|
+
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1613
|
+
subject: formatValueFile2(present.file),
|
|
1614
|
+
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`
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
if (findings.length > 0) {
|
|
1618
|
+
return findings;
|
|
1619
|
+
}
|
|
1620
|
+
return [
|
|
1621
|
+
{
|
|
1622
|
+
check: "provider-value-drift",
|
|
1623
|
+
severity: "pass",
|
|
1624
|
+
label: "Provider values",
|
|
1625
|
+
subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
|
|
1626
|
+
}
|
|
1627
|
+
];
|
|
1628
|
+
}
|
|
1268
1629
|
function renderDoctor(report) {
|
|
1269
1630
|
const rows = report.findings.map((finding) => ({
|
|
1270
1631
|
glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
|
|
@@ -1307,9 +1668,9 @@ var doctorCommand = defineCommand3({
|
|
|
1307
1668
|
|
|
1308
1669
|
// src/commands/encrypt.ts
|
|
1309
1670
|
import {
|
|
1310
|
-
formatValueFile as
|
|
1671
|
+
formatValueFile as formatValueFile4,
|
|
1311
1672
|
isSecret as isSecret3,
|
|
1312
|
-
openValue as
|
|
1673
|
+
openValue as openValue3,
|
|
1313
1674
|
PenvError as PenvError7,
|
|
1314
1675
|
parameterId as parameterId4,
|
|
1315
1676
|
sealValue as sealValue2
|
|
@@ -1317,7 +1678,7 @@ import {
|
|
|
1317
1678
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1318
1679
|
|
|
1319
1680
|
// src/commands/set.ts
|
|
1320
|
-
import { formatValueFile as
|
|
1681
|
+
import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
|
|
1321
1682
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1322
1683
|
function scopeFrom(options) {
|
|
1323
1684
|
if (options.local === true) {
|
|
@@ -1352,38 +1713,50 @@ function sealFor(project, file, value, parameter, environment) {
|
|
|
1352
1713
|
if (environment === void 0) {
|
|
1353
1714
|
throw new PenvError6(
|
|
1354
1715
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1355
|
-
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${
|
|
1716
|
+
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
|
|
1356
1717
|
"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."
|
|
1357
1718
|
);
|
|
1358
1719
|
}
|
|
1359
1720
|
return sealValue(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1360
1721
|
}
|
|
1361
|
-
async function
|
|
1362
|
-
const project =
|
|
1363
|
-
const
|
|
1364
|
-
const scope = targetScope(project, options, options.key);
|
|
1365
|
-
const environment = policyEnvironment(project, options);
|
|
1366
|
-
const secret = isSecret2(await project.provider.readMeta(ref), environment);
|
|
1722
|
+
async function sealAwareWrite(options) {
|
|
1723
|
+
const { project, provider, ref, scope, value, environment } = options;
|
|
1724
|
+
const secret = isSecret2(await provider.readMeta(ref), environment);
|
|
1367
1725
|
const file = {
|
|
1368
1726
|
namespace: ref.namespace,
|
|
1369
1727
|
name: ref.name,
|
|
1370
1728
|
scope,
|
|
1371
1729
|
encrypted: secret
|
|
1372
1730
|
};
|
|
1373
|
-
const stored = secret ? sealFor(project, file,
|
|
1374
|
-
await
|
|
1375
|
-
await
|
|
1376
|
-
return {
|
|
1731
|
+
const stored = secret ? sealFor(project, file, value, parameterId3(ref), environment) : value;
|
|
1732
|
+
await provider.write(file, stored);
|
|
1733
|
+
await provider.remove({ ...file, encrypted: !secret });
|
|
1734
|
+
return { encrypted: secret, location: formatValueFile3(file) };
|
|
1377
1735
|
}
|
|
1378
|
-
function
|
|
1736
|
+
async function runSet(options) {
|
|
1737
|
+
const project = openProject(options.cwd);
|
|
1738
|
+
const ref = refFromKey(options.key, project.config);
|
|
1739
|
+
const scope = targetScope(project, options, options.key);
|
|
1740
|
+
const environment = policyEnvironment(project, options);
|
|
1741
|
+
const { encrypted, location } = await sealAwareWrite({
|
|
1742
|
+
project,
|
|
1743
|
+
provider: project.provider,
|
|
1744
|
+
ref,
|
|
1745
|
+
scope,
|
|
1746
|
+
value: options.value,
|
|
1747
|
+
environment
|
|
1748
|
+
});
|
|
1749
|
+
return { parameter: options.key, location, encrypted };
|
|
1750
|
+
}
|
|
1751
|
+
function renderSet(result2) {
|
|
1379
1752
|
return formatRows([
|
|
1380
1753
|
{
|
|
1381
1754
|
glyph: CHECK,
|
|
1382
1755
|
label: "Wrote",
|
|
1383
|
-
subject: `${PENV_DIR}/${
|
|
1756
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1384
1757
|
// The `.enc` suffix says this already, but only to a reader who knows the
|
|
1385
1758
|
// grammar. Meta decided it, not the command line, so the command says so.
|
|
1386
|
-
...
|
|
1759
|
+
...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
1387
1760
|
}
|
|
1388
1761
|
]);
|
|
1389
1762
|
}
|
|
@@ -1460,7 +1833,7 @@ async function runEncrypt(options) {
|
|
|
1460
1833
|
const already = await readOne(project, sealed);
|
|
1461
1834
|
throw new PenvError7(
|
|
1462
1835
|
"PARAMETER_ABSENT",
|
|
1463
|
-
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${
|
|
1836
|
+
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile4(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
1464
1837
|
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."
|
|
1465
1838
|
);
|
|
1466
1839
|
}
|
|
@@ -1469,8 +1842,8 @@ async function runEncrypt(options) {
|
|
|
1469
1842
|
await project.provider.remove(plain);
|
|
1470
1843
|
return {
|
|
1471
1844
|
parameter,
|
|
1472
|
-
location:
|
|
1473
|
-
removed:
|
|
1845
|
+
location: formatValueFile4(sealed),
|
|
1846
|
+
removed: formatValueFile4(plain)
|
|
1474
1847
|
};
|
|
1475
1848
|
}
|
|
1476
1849
|
async function runDecrypt(options) {
|
|
@@ -1489,16 +1862,16 @@ async function runDecrypt(options) {
|
|
|
1489
1862
|
if (stored === void 0) {
|
|
1490
1863
|
throw new PenvError7(
|
|
1491
1864
|
"PARAMETER_ABSENT",
|
|
1492
|
-
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${
|
|
1865
|
+
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
1493
1866
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
1494
1867
|
);
|
|
1495
1868
|
}
|
|
1496
|
-
const opened =
|
|
1869
|
+
const opened = openValue3(sealed, stored, keySourceFor(project, environment));
|
|
1497
1870
|
if (opened.kind === "failed") {
|
|
1498
1871
|
throw new UndecryptableAt(
|
|
1499
1872
|
parameter,
|
|
1500
1873
|
environment,
|
|
1501
|
-
|
|
1874
|
+
formatValueFile4(sealed),
|
|
1502
1875
|
opened.failure.detail
|
|
1503
1876
|
);
|
|
1504
1877
|
}
|
|
@@ -1506,8 +1879,8 @@ async function runDecrypt(options) {
|
|
|
1506
1879
|
await project.provider.remove(sealed);
|
|
1507
1880
|
return {
|
|
1508
1881
|
parameter,
|
|
1509
|
-
location:
|
|
1510
|
-
removed:
|
|
1882
|
+
location: formatValueFile4(plain),
|
|
1883
|
+
removed: formatValueFile4(sealed)
|
|
1511
1884
|
};
|
|
1512
1885
|
}
|
|
1513
1886
|
var UndecryptableAt = class extends PenvError7 {
|
|
@@ -1519,13 +1892,13 @@ var UndecryptableAt = class extends PenvError7 {
|
|
|
1519
1892
|
);
|
|
1520
1893
|
}
|
|
1521
1894
|
};
|
|
1522
|
-
function renderReseal(
|
|
1895
|
+
function renderReseal(result2, verb) {
|
|
1523
1896
|
return formatRows([
|
|
1524
1897
|
{
|
|
1525
1898
|
glyph: CHECK,
|
|
1526
1899
|
label: verb,
|
|
1527
|
-
subject: `${PENV_DIR}/${
|
|
1528
|
-
detail: `${PENV_DIR}/${
|
|
1900
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1901
|
+
detail: `${PENV_DIR}/${result2.removed} removed`
|
|
1529
1902
|
}
|
|
1530
1903
|
]);
|
|
1531
1904
|
}
|
|
@@ -1548,8 +1921,8 @@ var encryptCommand = defineCommand5({
|
|
|
1548
1921
|
args: SCOPE_ARGS,
|
|
1549
1922
|
run({ args }) {
|
|
1550
1923
|
return guard(async () => {
|
|
1551
|
-
const
|
|
1552
|
-
write(renderReseal(
|
|
1924
|
+
const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1925
|
+
write(renderReseal(result2, "Encrypted"));
|
|
1553
1926
|
});
|
|
1554
1927
|
}
|
|
1555
1928
|
});
|
|
@@ -1558,15 +1931,15 @@ var decryptCommand = defineCommand5({
|
|
|
1558
1931
|
args: SCOPE_ARGS,
|
|
1559
1932
|
run({ args }) {
|
|
1560
1933
|
return guard(async () => {
|
|
1561
|
-
const
|
|
1562
|
-
write(renderReseal(
|
|
1934
|
+
const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1935
|
+
write(renderReseal(result2, "Decrypted"));
|
|
1563
1936
|
});
|
|
1564
1937
|
}
|
|
1565
1938
|
});
|
|
1566
1939
|
|
|
1567
1940
|
// src/commands/generate.ts
|
|
1568
1941
|
import { writeFileSync } from "fs";
|
|
1569
|
-
import { isAbsolute, relative, resolve as
|
|
1942
|
+
import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
1570
1943
|
import {
|
|
1571
1944
|
checkNameCollisions as checkNameCollisions3,
|
|
1572
1945
|
effectiveMeta as effectiveMeta2,
|
|
@@ -1625,7 +1998,7 @@ function runGenerate(options) {
|
|
|
1625
1998
|
const project = openProject(options.cwd);
|
|
1626
1999
|
const environment = targetEnvironment(project, options.environment);
|
|
1627
2000
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
1628
|
-
const file = options.out === void 0 ?
|
|
2001
|
+
const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
|
|
1629
2002
|
writeFileSync(file, serializeDotenv(entries), "utf8");
|
|
1630
2003
|
return { file, environment, entries: entries.length, decrypted };
|
|
1631
2004
|
}
|
|
@@ -1633,20 +2006,20 @@ function displayPath(cwd, file) {
|
|
|
1633
2006
|
const rel = relative(cwd, file);
|
|
1634
2007
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
1635
2008
|
}
|
|
1636
|
-
function renderGenerate(
|
|
2009
|
+
function renderGenerate(result2, cwd) {
|
|
1637
2010
|
const rows = [
|
|
1638
2011
|
{
|
|
1639
2012
|
glyph: CHECK,
|
|
1640
2013
|
label: "Generated",
|
|
1641
|
-
subject: displayPath(cwd,
|
|
1642
|
-
detail: `${
|
|
2014
|
+
subject: displayPath(cwd, result2.file),
|
|
2015
|
+
detail: `${result2.entries} variables for environment ${result2.environment}`
|
|
1643
2016
|
}
|
|
1644
2017
|
];
|
|
1645
|
-
if (
|
|
2018
|
+
if (result2.decrypted > 0) {
|
|
1646
2019
|
rows.push({
|
|
1647
2020
|
glyph: WARN,
|
|
1648
2021
|
label: "Decrypted",
|
|
1649
|
-
subject: `${
|
|
2022
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
1650
2023
|
detail: "written as plaintext into the artifact"
|
|
1651
2024
|
});
|
|
1652
2025
|
}
|
|
@@ -1665,13 +2038,13 @@ var generateCommand = defineCommand6({
|
|
|
1665
2038
|
run({ args }) {
|
|
1666
2039
|
return guard(async () => {
|
|
1667
2040
|
const cwd = process.cwd();
|
|
1668
|
-
const
|
|
2041
|
+
const result2 = runGenerate({
|
|
1669
2042
|
cwd,
|
|
1670
2043
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
1671
2044
|
...args.out === void 0 ? {} : { out: args.out },
|
|
1672
2045
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1673
2046
|
});
|
|
1674
|
-
write(renderGenerate(
|
|
2047
|
+
write(renderGenerate(result2, cwd));
|
|
1675
2048
|
});
|
|
1676
2049
|
}
|
|
1677
2050
|
});
|
|
@@ -1762,10 +2135,10 @@ var getCommand = defineCommand7({
|
|
|
1762
2135
|
|
|
1763
2136
|
// src/commands/import.ts
|
|
1764
2137
|
import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1765
|
-
import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as
|
|
2138
|
+
import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
|
|
1766
2139
|
import {
|
|
1767
2140
|
accessPath as accessPath4,
|
|
1768
|
-
assertNever,
|
|
2141
|
+
assertNever as assertNever2,
|
|
1769
2142
|
checkNameCollisions as checkNameCollisions4,
|
|
1770
2143
|
FilenameGrammarError,
|
|
1771
2144
|
findConfigFile,
|
|
@@ -1886,7 +2259,7 @@ function hasImportsBlock(cwd) {
|
|
|
1886
2259
|
|
|
1887
2260
|
// src/commands/init.ts
|
|
1888
2261
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1889
|
-
import { dirname as
|
|
2262
|
+
import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
|
|
1890
2263
|
import { createInterface } from "readline/promises";
|
|
1891
2264
|
import {
|
|
1892
2265
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
@@ -2356,7 +2729,7 @@ function renderTsconfig(target, alias) {
|
|
|
2356
2729
|
`;
|
|
2357
2730
|
}
|
|
2358
2731
|
function ensurePenvDir(root) {
|
|
2359
|
-
const dir =
|
|
2732
|
+
const dir = resolve4(root, PENV_DIR2);
|
|
2360
2733
|
if (existsSync2(dir)) {
|
|
2361
2734
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
2362
2735
|
}
|
|
@@ -2373,7 +2746,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2373
2746
|
note: "(yours \u2014 penv never regenerates it)"
|
|
2374
2747
|
};
|
|
2375
2748
|
}
|
|
2376
|
-
mkdirSync(
|
|
2749
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
2377
2750
|
writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
|
|
2378
2751
|
return {
|
|
2379
2752
|
target: "schema",
|
|
@@ -2461,20 +2834,20 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2461
2834
|
];
|
|
2462
2835
|
}
|
|
2463
2836
|
function runInit(options) {
|
|
2464
|
-
const root =
|
|
2837
|
+
const root = resolve4(options.cwd);
|
|
2465
2838
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
2466
2839
|
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
2467
2840
|
}
|
|
2468
|
-
function renderInit(
|
|
2469
|
-
const steps =
|
|
2841
|
+
function renderInit(result2) {
|
|
2842
|
+
const steps = result2.steps.map((step) => {
|
|
2470
2843
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
2471
2844
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
2472
2845
|
});
|
|
2473
2846
|
return [
|
|
2474
2847
|
...formatSteps(steps),
|
|
2475
2848
|
"",
|
|
2476
|
-
`Done. Declare your parameters in ${
|
|
2477
|
-
...
|
|
2849
|
+
`Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
|
|
2850
|
+
...result2.decisions.environments.length === 0 ? [
|
|
2478
2851
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
2479
2852
|
] : []
|
|
2480
2853
|
];
|
|
@@ -2513,7 +2886,7 @@ var initCommand = defineCommand8({
|
|
|
2513
2886
|
},
|
|
2514
2887
|
run({ args }) {
|
|
2515
2888
|
return guard(async () => {
|
|
2516
|
-
const root =
|
|
2889
|
+
const root = resolve4(process.cwd());
|
|
2517
2890
|
const environments = environmentsFromFlag(args.env);
|
|
2518
2891
|
const plan2 = planInit(root, {
|
|
2519
2892
|
...args.schema === void 0 ? {} : { schema: args.schema },
|
|
@@ -2647,7 +3020,7 @@ function environmentOf(scope) {
|
|
|
2647
3020
|
case "local":
|
|
2648
3021
|
return void 0;
|
|
2649
3022
|
default:
|
|
2650
|
-
return
|
|
3023
|
+
return assertNever2(scope, "scope");
|
|
2651
3024
|
}
|
|
2652
3025
|
}
|
|
2653
3026
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -2660,7 +3033,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
2660
3033
|
case "environment-local":
|
|
2661
3034
|
return scope;
|
|
2662
3035
|
default:
|
|
2663
|
-
return
|
|
3036
|
+
return assertNever2(scope, "scope");
|
|
2664
3037
|
}
|
|
2665
3038
|
}
|
|
2666
3039
|
function explicitEnvironment(options, source, config) {
|
|
@@ -2719,8 +3092,8 @@ function configInEffect(cwd, environment) {
|
|
|
2719
3092
|
return { config: openProject(cwd).config, decisions };
|
|
2720
3093
|
}
|
|
2721
3094
|
function importDotenv(options) {
|
|
2722
|
-
const cwd =
|
|
2723
|
-
const file = isAbsolute2(options.file) ? options.file :
|
|
3095
|
+
const cwd = resolve5(options.cwd);
|
|
3096
|
+
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
2724
3097
|
if (!existsSync3(file)) {
|
|
2725
3098
|
throw new PenvError11(
|
|
2726
3099
|
"IMPORT_FILE_MISSING",
|
|
@@ -2803,19 +3176,19 @@ function skippedValidationStep(environments) {
|
|
|
2803
3176
|
note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
|
|
2804
3177
|
};
|
|
2805
3178
|
}
|
|
2806
|
-
function renderImport(
|
|
2807
|
-
const steps = [{ glyph: CHECK, text: `Found ${
|
|
2808
|
-
if (
|
|
2809
|
-
const plural =
|
|
3179
|
+
function renderImport(result2, validation) {
|
|
3180
|
+
const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
|
|
3181
|
+
if (result2.orphanComments > 0) {
|
|
3182
|
+
const plural = result2.orphanComments === 1 ? "comment" : "comments";
|
|
2810
3183
|
steps.push({
|
|
2811
3184
|
glyph: WARN,
|
|
2812
|
-
text: `Dropped ${
|
|
3185
|
+
text: `Dropped ${result2.orphanComments} orphan ${plural}`,
|
|
2813
3186
|
note: "attached to no variable, so nothing to describe"
|
|
2814
3187
|
});
|
|
2815
3188
|
}
|
|
2816
|
-
for (const step of
|
|
3189
|
+
for (const step of result2.steps) {
|
|
2817
3190
|
if (step.target === "schema" && step.action === "kept") {
|
|
2818
|
-
steps.push(keptSchemaStep(step,
|
|
3191
|
+
steps.push(keptSchemaStep(step, result2.variables));
|
|
2819
3192
|
continue;
|
|
2820
3193
|
}
|
|
2821
3194
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
@@ -2823,10 +3196,10 @@ function renderImport(result, validation) {
|
|
|
2823
3196
|
step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
|
|
2824
3197
|
);
|
|
2825
3198
|
}
|
|
2826
|
-
steps.push({ glyph: CHECK, text: `Created ${displayPath2(
|
|
3199
|
+
steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
|
|
2827
3200
|
const lines = formatSteps(steps);
|
|
2828
3201
|
if (validation === void 0) {
|
|
2829
|
-
lines.push(...formatSteps([skippedValidationStep(
|
|
3202
|
+
lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
|
|
2830
3203
|
} else if (validation.ok) {
|
|
2831
3204
|
lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
|
|
2832
3205
|
} else {
|
|
@@ -2871,11 +3244,11 @@ import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError12 } from "@penvhq/c
|
|
|
2871
3244
|
import { defineCommand as defineCommand10 } from "citty";
|
|
2872
3245
|
|
|
2873
3246
|
// src/keychain.ts
|
|
2874
|
-
import { createRequire } from "module";
|
|
3247
|
+
import { createRequire as createRequire2 } from "module";
|
|
2875
3248
|
var cached;
|
|
2876
3249
|
function entryConstructor() {
|
|
2877
3250
|
if (cached === void 0) {
|
|
2878
|
-
const require2 =
|
|
3251
|
+
const require2 = createRequire2(import.meta.url);
|
|
2879
3252
|
cached = require2("@napi-rs/keyring").Entry;
|
|
2880
3253
|
}
|
|
2881
3254
|
return cached;
|
|
@@ -2939,19 +3312,19 @@ function runKeyCreate(options) {
|
|
|
2939
3312
|
key
|
|
2940
3313
|
};
|
|
2941
3314
|
}
|
|
2942
|
-
function renderKeyCreate(
|
|
2943
|
-
if (
|
|
3315
|
+
function renderKeyCreate(result2) {
|
|
3316
|
+
if (result2.source === "keychain") {
|
|
2944
3317
|
return [
|
|
2945
|
-
`A new key for environment ${
|
|
3318
|
+
`A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
2946
3319
|
"",
|
|
2947
3320
|
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
|
|
2948
3321
|
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
2949
3322
|
];
|
|
2950
3323
|
}
|
|
2951
3324
|
return [
|
|
2952
|
-
`A new key for environment ${
|
|
3325
|
+
`A new key for environment ${result2.environment}. penv did not store it.`,
|
|
2953
3326
|
"",
|
|
2954
|
-
` ${
|
|
3327
|
+
` ${result2.variable}=${result2.key}`,
|
|
2955
3328
|
"",
|
|
2956
3329
|
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
|
|
2957
3330
|
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
|
|
@@ -2988,7 +3361,7 @@ var keyCommand = defineCommand10({
|
|
|
2988
3361
|
});
|
|
2989
3362
|
|
|
2990
3363
|
// src/commands/list.ts
|
|
2991
|
-
import { assertNever as
|
|
3364
|
+
import { assertNever as assertNever3, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
|
|
2992
3365
|
import { defineCommand as defineCommand11 } from "citty";
|
|
2993
3366
|
function scopeLabel2(scope) {
|
|
2994
3367
|
switch (scope.kind) {
|
|
@@ -3001,7 +3374,7 @@ function scopeLabel2(scope) {
|
|
|
3001
3374
|
case "unscoped":
|
|
3002
3375
|
return "default";
|
|
3003
3376
|
default:
|
|
3004
|
-
return
|
|
3377
|
+
return assertNever3(scope, "scope");
|
|
3005
3378
|
}
|
|
3006
3379
|
}
|
|
3007
3380
|
async function runList(options) {
|
|
@@ -3023,11 +3396,11 @@ async function runList(options) {
|
|
|
3023
3396
|
}
|
|
3024
3397
|
return { environment, parameters };
|
|
3025
3398
|
}
|
|
3026
|
-
function renderList(
|
|
3027
|
-
if (
|
|
3028
|
-
return [`No parameters in ${PENV_DIR}/ for environment ${
|
|
3399
|
+
function renderList(result2) {
|
|
3400
|
+
if (result2.parameters.length === 0) {
|
|
3401
|
+
return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
|
|
3029
3402
|
}
|
|
3030
|
-
return columns(
|
|
3403
|
+
return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
|
|
3031
3404
|
}
|
|
3032
3405
|
var listCommand = defineCommand11({
|
|
3033
3406
|
meta: { name: "list", description: "List parameters" },
|
|
@@ -3037,11 +3410,11 @@ var listCommand = defineCommand11({
|
|
|
3037
3410
|
},
|
|
3038
3411
|
run({ args }) {
|
|
3039
3412
|
return guard(async () => {
|
|
3040
|
-
const
|
|
3413
|
+
const result2 = await runList({
|
|
3041
3414
|
cwd: process.cwd(),
|
|
3042
3415
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
3043
3416
|
});
|
|
3044
|
-
write(args.json === true ? [JSON.stringify(
|
|
3417
|
+
write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
|
|
3045
3418
|
});
|
|
3046
3419
|
}
|
|
3047
3420
|
});
|
|
@@ -3050,8 +3423,8 @@ var listCommand = defineCommand11({
|
|
|
3050
3423
|
import {
|
|
3051
3424
|
accessPath as accessPath5,
|
|
3052
3425
|
formatMetaFile,
|
|
3053
|
-
formatValueFile as
|
|
3054
|
-
openValue as
|
|
3426
|
+
formatValueFile as formatValueFile5,
|
|
3427
|
+
openValue as openValue4,
|
|
3055
3428
|
PenvError as PenvError13,
|
|
3056
3429
|
parameterId as parameterId5,
|
|
3057
3430
|
sealValue as sealValue3
|
|
@@ -3073,16 +3446,16 @@ async function planFile(project, source, target, parameter) {
|
|
|
3073
3446
|
if (environment === void 0) {
|
|
3074
3447
|
throw new PenvError13(
|
|
3075
3448
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3076
|
-
`${PENV_DIR}/${
|
|
3449
|
+
`${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3077
3450
|
"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."
|
|
3078
3451
|
);
|
|
3079
3452
|
}
|
|
3080
3453
|
const keys = keySourceFor(project, environment);
|
|
3081
|
-
const opened =
|
|
3454
|
+
const opened = openValue4(source, stored, keys);
|
|
3082
3455
|
if (opened.kind === "failed") {
|
|
3083
3456
|
throw new PenvError13(
|
|
3084
3457
|
"VALUE_UNDECRYPTABLE",
|
|
3085
|
-
`${PENV_DIR}/${
|
|
3458
|
+
`${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3086
3459
|
"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."
|
|
3087
3460
|
);
|
|
3088
3461
|
}
|
|
@@ -3150,8 +3523,8 @@ async function runMove(options) {
|
|
|
3150
3523
|
from: parameterId5(from),
|
|
3151
3524
|
to: parameterId5(to),
|
|
3152
3525
|
files: planned.map((file) => ({
|
|
3153
|
-
from:
|
|
3154
|
-
to:
|
|
3526
|
+
from: formatValueFile5(file.source),
|
|
3527
|
+
to: formatValueFile5(file.target),
|
|
3155
3528
|
resealed: file.resealed
|
|
3156
3529
|
})),
|
|
3157
3530
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
@@ -3161,20 +3534,20 @@ async function runMove(options) {
|
|
|
3161
3534
|
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
|
|
3162
3535
|
};
|
|
3163
3536
|
}
|
|
3164
|
-
function renderMove(
|
|
3165
|
-
const rows =
|
|
3537
|
+
function renderMove(result2) {
|
|
3538
|
+
const rows = result2.files.map((file) => ({
|
|
3166
3539
|
glyph: CHECK,
|
|
3167
3540
|
label: "Moved",
|
|
3168
3541
|
subject: `${PENV_DIR}/${file.to}`,
|
|
3169
3542
|
...file.resealed ? { detail: "re-sealed for its new address" } : {}
|
|
3170
3543
|
}));
|
|
3171
|
-
if (
|
|
3172
|
-
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${
|
|
3544
|
+
if (result2.meta !== void 0) {
|
|
3545
|
+
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
|
|
3173
3546
|
}
|
|
3174
3547
|
const lines = formatRows(rows);
|
|
3175
3548
|
lines.push(
|
|
3176
3549
|
"",
|
|
3177
|
-
` .penv/env.ts still declares \`${
|
|
3550
|
+
` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
|
|
3178
3551
|
" or `penv validate` will report the value as unused and the declaration as unset."
|
|
3179
3552
|
);
|
|
3180
3553
|
return lines;
|
|
@@ -3200,9 +3573,86 @@ var mvCommand = defineCommand12({
|
|
|
3200
3573
|
}
|
|
3201
3574
|
});
|
|
3202
3575
|
|
|
3203
|
-
// src/commands/
|
|
3204
|
-
import { formatValueFile as formatValueFile5 } from "@penvhq/core";
|
|
3576
|
+
// src/commands/pull.ts
|
|
3205
3577
|
import { defineCommand as defineCommand13 } from "citty";
|
|
3578
|
+
async function runPull(options) {
|
|
3579
|
+
const project = openProject(options.cwd);
|
|
3580
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3581
|
+
const source = await sourceProviderFor(project, environment);
|
|
3582
|
+
if (source.type === LOCAL_TREE_TYPE) {
|
|
3583
|
+
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3584
|
+
}
|
|
3585
|
+
const tree = localTree(project);
|
|
3586
|
+
const files = await source.list();
|
|
3587
|
+
let values = 0;
|
|
3588
|
+
for (const file of files) {
|
|
3589
|
+
const value = await source.read(file);
|
|
3590
|
+
if (value === void 0) {
|
|
3591
|
+
continue;
|
|
3592
|
+
}
|
|
3593
|
+
tree.writeSync(file, value);
|
|
3594
|
+
values += 1;
|
|
3595
|
+
}
|
|
3596
|
+
const refs = refsFrom(files);
|
|
3597
|
+
let meta = 0;
|
|
3598
|
+
for (const ref of refs) {
|
|
3599
|
+
const block = await source.readMeta(ref);
|
|
3600
|
+
if (block === void 0) {
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
tree.writeMetaSync(ref, block);
|
|
3604
|
+
meta += 1;
|
|
3605
|
+
}
|
|
3606
|
+
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
3607
|
+
}
|
|
3608
|
+
function renderPull(result2) {
|
|
3609
|
+
if (result2.localSource) {
|
|
3610
|
+
return formatRows([
|
|
3611
|
+
{
|
|
3612
|
+
glyph: CHECK,
|
|
3613
|
+
label: "Nothing to pull",
|
|
3614
|
+
subject: `environment ${result2.environment} has no separate source of truth`,
|
|
3615
|
+
detail: "its values live in the local .penv tree already"
|
|
3616
|
+
}
|
|
3617
|
+
]);
|
|
3618
|
+
}
|
|
3619
|
+
return formatRows([
|
|
3620
|
+
{
|
|
3621
|
+
glyph: CHECK,
|
|
3622
|
+
label: "Pulled",
|
|
3623
|
+
subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
|
|
3624
|
+
detail: `from the ${result2.source} provider for environment ${result2.environment}`
|
|
3625
|
+
},
|
|
3626
|
+
{
|
|
3627
|
+
glyph: CHECK,
|
|
3628
|
+
label: "Parameters",
|
|
3629
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
|
|
3630
|
+
detail: "written into the local .penv tree"
|
|
3631
|
+
}
|
|
3632
|
+
]);
|
|
3633
|
+
}
|
|
3634
|
+
var pullCommand = defineCommand13({
|
|
3635
|
+
meta: {
|
|
3636
|
+
name: "pull",
|
|
3637
|
+
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3638
|
+
},
|
|
3639
|
+
args: {
|
|
3640
|
+
env: { type: "string", description: "The environment to pull" }
|
|
3641
|
+
},
|
|
3642
|
+
run({ args }) {
|
|
3643
|
+
return guard(async () => {
|
|
3644
|
+
const result2 = await runPull({
|
|
3645
|
+
cwd: process.cwd(),
|
|
3646
|
+
...args.env === void 0 ? {} : { environment: args.env }
|
|
3647
|
+
});
|
|
3648
|
+
write(renderPull(result2));
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
|
|
3653
|
+
// src/commands/remove.ts
|
|
3654
|
+
import { formatValueFile as formatValueFile6 } from "@penvhq/core";
|
|
3655
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
3206
3656
|
async function runRemove(options) {
|
|
3207
3657
|
const project = openProject(options.cwd);
|
|
3208
3658
|
const ref = refFromKey(options.key);
|
|
@@ -3219,17 +3669,17 @@ async function runRemove(options) {
|
|
|
3219
3669
|
continue;
|
|
3220
3670
|
}
|
|
3221
3671
|
await project.provider.remove(file);
|
|
3222
|
-
removed.push(
|
|
3672
|
+
removed.push(formatValueFile6(file));
|
|
3223
3673
|
}
|
|
3224
3674
|
return {
|
|
3225
3675
|
parameter: options.key,
|
|
3226
3676
|
removed,
|
|
3227
|
-
considered: files.map((file) =>
|
|
3677
|
+
considered: files.map((file) => formatValueFile6(file))
|
|
3228
3678
|
};
|
|
3229
3679
|
}
|
|
3230
|
-
function renderRemove(
|
|
3231
|
-
if (
|
|
3232
|
-
const first =
|
|
3680
|
+
function renderRemove(result2) {
|
|
3681
|
+
if (result2.removed.length === 0) {
|
|
3682
|
+
const first = result2.considered[0] ?? result2.parameter;
|
|
3233
3683
|
return formatRows([
|
|
3234
3684
|
{
|
|
3235
3685
|
glyph: WARN,
|
|
@@ -3239,14 +3689,14 @@ function renderRemove(result) {
|
|
|
3239
3689
|
}
|
|
3240
3690
|
]);
|
|
3241
3691
|
}
|
|
3242
|
-
const rows =
|
|
3692
|
+
const rows = result2.removed.map((location) => ({
|
|
3243
3693
|
glyph: CHECK,
|
|
3244
3694
|
label: "Removed",
|
|
3245
3695
|
subject: `${PENV_DIR}/${location}`
|
|
3246
3696
|
}));
|
|
3247
3697
|
return formatRows(rows);
|
|
3248
3698
|
}
|
|
3249
|
-
var removeCommand =
|
|
3699
|
+
var removeCommand = defineCommand14({
|
|
3250
3700
|
meta: { name: "remove", description: "Delete a parameter" },
|
|
3251
3701
|
args: {
|
|
3252
3702
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -3272,11 +3722,197 @@ var removeCommand = defineCommand13({
|
|
|
3272
3722
|
}
|
|
3273
3723
|
});
|
|
3274
3724
|
|
|
3725
|
+
// src/commands/rotate.ts
|
|
3726
|
+
import {
|
|
3727
|
+
beginRotation,
|
|
3728
|
+
completeRotation,
|
|
3729
|
+
PenvError as PenvError14,
|
|
3730
|
+
retainsPrevious,
|
|
3731
|
+
rotationOf as rotationOf2
|
|
3732
|
+
} from "@penvhq/core";
|
|
3733
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
3734
|
+
function rotatingFile(ref, environment) {
|
|
3735
|
+
return {
|
|
3736
|
+
namespace: ref.namespace,
|
|
3737
|
+
name: ref.name,
|
|
3738
|
+
scope: { kind: "environment", environment },
|
|
3739
|
+
encrypted: false
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
3743
|
+
if (provider.type === LOCAL_TREE_TYPE) {
|
|
3744
|
+
await sealAwareWrite({
|
|
3745
|
+
project,
|
|
3746
|
+
provider,
|
|
3747
|
+
ref,
|
|
3748
|
+
scope: { kind: "environment", environment },
|
|
3749
|
+
value,
|
|
3750
|
+
environment
|
|
3751
|
+
});
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
await provider.write(rotatingFile(ref, environment), value);
|
|
3755
|
+
}
|
|
3756
|
+
function requireNewValue(value, phase, key) {
|
|
3757
|
+
if (value === void 0) {
|
|
3758
|
+
throw new PenvError14(
|
|
3759
|
+
"ROTATION_NO_VALUE",
|
|
3760
|
+
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3761
|
+
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
3762
|
+
);
|
|
3763
|
+
}
|
|
3764
|
+
return value;
|
|
3765
|
+
}
|
|
3766
|
+
function requireRetaining(provider, environment) {
|
|
3767
|
+
if (!retainsPrevious(provider)) {
|
|
3768
|
+
throw new PenvError14(
|
|
3769
|
+
"ROTATION_NOT_RETAINING",
|
|
3770
|
+
`A dual-valid rotation needs the previous value to stay readable during the grace window, and the \`${provider.type}\` provider for environment ${environment} does not retain it`,
|
|
3771
|
+
"Point this environment at a provider that keeps prior versions (its `readPrevious` is what penv reads during the window), or, if a momentary overlap is not required, declare the parameter `atomic-cutover` in its meta and flip it in one step."
|
|
3772
|
+
);
|
|
3773
|
+
}
|
|
3774
|
+
return provider;
|
|
3775
|
+
}
|
|
3776
|
+
async function runRotate(options) {
|
|
3777
|
+
const project = openProject(options.cwd);
|
|
3778
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3779
|
+
const ref = refFromKey(options.key, project.config);
|
|
3780
|
+
const provider = await sourceProviderFor(project, environment);
|
|
3781
|
+
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3782
|
+
const before = await provider.readMeta(ref);
|
|
3783
|
+
const { mechanism } = rotationOf2(before, environment);
|
|
3784
|
+
if (mechanism === void 0) {
|
|
3785
|
+
throw new PenvError14(
|
|
3786
|
+
"ROTATION_NO_MECHANISM",
|
|
3787
|
+
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3788
|
+
'Set `rotationMechanism` in the parameter\'s meta to `"dual-valid"` (a grace-window overlap) or `"atomic-cutover"` (a single flip), then run `penv rotate` again.'
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
const begin = options.begin === true;
|
|
3792
|
+
const complete = options.complete === true;
|
|
3793
|
+
if (mechanism === "atomic-cutover") {
|
|
3794
|
+
if (begin || complete) {
|
|
3795
|
+
throw new PenvError14(
|
|
3796
|
+
"ROTATION_MECHANISM_MISMATCH",
|
|
3797
|
+
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3798
|
+
"Run `penv rotate <key> <value>` with no phase flag to flip it. `--begin`/`--complete` bracket a dual-valid grace window, which atomic-cutover has none of."
|
|
3799
|
+
);
|
|
3800
|
+
}
|
|
3801
|
+
const value = requireNewValue(options.value, "cutover", options.key);
|
|
3802
|
+
await writeRotatedValue(project, provider, ref, environment, value);
|
|
3803
|
+
const after2 = completeRotation(before, environment, nowIso);
|
|
3804
|
+
await provider.writeMeta(ref, after2);
|
|
3805
|
+
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
3806
|
+
}
|
|
3807
|
+
const retaining = requireRetaining(provider, environment);
|
|
3808
|
+
if (begin === complete) {
|
|
3809
|
+
throw new PenvError14(
|
|
3810
|
+
"ROTATION_PHASE_REQUIRED",
|
|
3811
|
+
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3812
|
+
"`--begin` writes the new value and opens the grace window; `--complete` closes it once every reader has moved to the new value. Run them in that order, one at a time."
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
if (begin) {
|
|
3816
|
+
const value = requireNewValue(options.value, "begin", options.key);
|
|
3817
|
+
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
3818
|
+
const after2 = beginRotation(before, environment, nowIso);
|
|
3819
|
+
await retaining.writeMeta(ref, after2);
|
|
3820
|
+
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
3821
|
+
}
|
|
3822
|
+
const after = completeRotation(before, environment, nowIso);
|
|
3823
|
+
await retaining.writeMeta(ref, after);
|
|
3824
|
+
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
3825
|
+
}
|
|
3826
|
+
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
3827
|
+
const { state, rotatingSince, lastRotated } = rotationOf2(after, environment);
|
|
3828
|
+
return {
|
|
3829
|
+
parameter: [...ref.namespace, ref.name].join("/"),
|
|
3830
|
+
environment,
|
|
3831
|
+
mechanism,
|
|
3832
|
+
phase,
|
|
3833
|
+
source,
|
|
3834
|
+
wroteValue,
|
|
3835
|
+
state,
|
|
3836
|
+
rotatingSince,
|
|
3837
|
+
lastRotated
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
function renderRotate(result2) {
|
|
3841
|
+
if (result2.phase === "begin") {
|
|
3842
|
+
return formatRows([
|
|
3843
|
+
{
|
|
3844
|
+
glyph: CHECK,
|
|
3845
|
+
label: "Rotating",
|
|
3846
|
+
subject: result2.parameter,
|
|
3847
|
+
detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
|
|
3848
|
+
},
|
|
3849
|
+
{
|
|
3850
|
+
glyph: CHECK,
|
|
3851
|
+
label: "Previous",
|
|
3852
|
+
subject: "still readable",
|
|
3853
|
+
detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
|
|
3854
|
+
}
|
|
3855
|
+
]);
|
|
3856
|
+
}
|
|
3857
|
+
if (result2.phase === "complete") {
|
|
3858
|
+
return formatRows([
|
|
3859
|
+
{
|
|
3860
|
+
glyph: CHECK,
|
|
3861
|
+
label: "Rotated",
|
|
3862
|
+
subject: result2.parameter,
|
|
3863
|
+
detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3864
|
+
}
|
|
3865
|
+
]);
|
|
3866
|
+
}
|
|
3867
|
+
return formatRows([
|
|
3868
|
+
{
|
|
3869
|
+
glyph: CHECK,
|
|
3870
|
+
label: "Rotated",
|
|
3871
|
+
subject: result2.parameter,
|
|
3872
|
+
detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3873
|
+
}
|
|
3874
|
+
]);
|
|
3875
|
+
}
|
|
3876
|
+
var rotateCommand = defineCommand15({
|
|
3877
|
+
meta: {
|
|
3878
|
+
name: "rotate",
|
|
3879
|
+
description: "Rotate a parameter's value by the mechanism its meta declares"
|
|
3880
|
+
},
|
|
3881
|
+
args: {
|
|
3882
|
+
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
3883
|
+
value: {
|
|
3884
|
+
type: "positional",
|
|
3885
|
+
required: false,
|
|
3886
|
+
description: "The new value; read from stdin if omitted. Not needed with --complete"
|
|
3887
|
+
},
|
|
3888
|
+
env: { type: "string", description: "The environment to rotate in" },
|
|
3889
|
+
begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
|
|
3890
|
+
complete: { type: "boolean", description: "Close a dual-valid grace window" }
|
|
3891
|
+
},
|
|
3892
|
+
run({ args }) {
|
|
3893
|
+
return guard(async () => {
|
|
3894
|
+
const value = args.complete === true ? void 0 : args.value ?? await readStdin();
|
|
3895
|
+
write(
|
|
3896
|
+
renderRotate(
|
|
3897
|
+
await runRotate({
|
|
3898
|
+
cwd: process.cwd(),
|
|
3899
|
+
key: args.key,
|
|
3900
|
+
...value === void 0 ? {} : { value },
|
|
3901
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
3902
|
+
...args.begin === void 0 ? {} : { begin: args.begin },
|
|
3903
|
+
...args.complete === void 0 ? {} : { complete: args.complete }
|
|
3904
|
+
})
|
|
3905
|
+
)
|
|
3906
|
+
);
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
});
|
|
3910
|
+
|
|
3275
3911
|
// src/commands/watch.ts
|
|
3276
3912
|
import { existsSync as existsSync4, watch } from "fs";
|
|
3277
|
-
import { basename as basename2, dirname as
|
|
3913
|
+
import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
|
|
3278
3914
|
import { schemaFileOf as schemaFileOf4, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
|
|
3279
|
-
import { defineCommand as
|
|
3915
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
3280
3916
|
var DEBOUNCE_MS = 100;
|
|
3281
3917
|
function runWatch(options) {
|
|
3282
3918
|
const project = openProject(options.cwd);
|
|
@@ -3297,12 +3933,12 @@ function runWatch(options) {
|
|
|
3297
3933
|
}
|
|
3298
3934
|
running = true;
|
|
3299
3935
|
try {
|
|
3300
|
-
const
|
|
3936
|
+
const result2 = await runValidate({
|
|
3301
3937
|
cwd: options.cwd,
|
|
3302
3938
|
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
3303
3939
|
});
|
|
3304
3940
|
if (!closed) {
|
|
3305
|
-
options.onResult?.(
|
|
3941
|
+
options.onResult?.(result2);
|
|
3306
3942
|
}
|
|
3307
3943
|
} catch (error) {
|
|
3308
3944
|
if (!closed) {
|
|
@@ -3333,7 +3969,7 @@ function runWatch(options) {
|
|
|
3333
3969
|
watcher.close();
|
|
3334
3970
|
}
|
|
3335
3971
|
function armRecovery(target, recursive, only) {
|
|
3336
|
-
const parent =
|
|
3972
|
+
const parent = dirname4(target);
|
|
3337
3973
|
const name = basename2(target);
|
|
3338
3974
|
let recovery;
|
|
3339
3975
|
try {
|
|
@@ -3407,10 +4043,10 @@ function runWatch(options) {
|
|
|
3407
4043
|
watchers.add(watcher);
|
|
3408
4044
|
}
|
|
3409
4045
|
addWatcher(project.penvDir, true);
|
|
3410
|
-
addWatcher(
|
|
4046
|
+
addWatcher(dirname4(project.configFile), false, configFile);
|
|
3411
4047
|
if (schemaInsideTree2(project.config) === void 0) {
|
|
3412
|
-
const schemaFile =
|
|
3413
|
-
addWatcher(
|
|
4048
|
+
const schemaFile = resolve6(project.root, schemaFileOf4(project.config));
|
|
4049
|
+
addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
|
|
3414
4050
|
}
|
|
3415
4051
|
void validate();
|
|
3416
4052
|
return {
|
|
@@ -3426,8 +4062,8 @@ function runWatch(options) {
|
|
|
3426
4062
|
}
|
|
3427
4063
|
};
|
|
3428
4064
|
}
|
|
3429
|
-
function renderWatch(
|
|
3430
|
-
return ["", ...renderValidate(
|
|
4065
|
+
function renderWatch(result2) {
|
|
4066
|
+
return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
|
|
3431
4067
|
}
|
|
3432
4068
|
function renderDrift(drift, environment) {
|
|
3433
4069
|
const rows = [
|
|
@@ -3453,7 +4089,7 @@ function renderDrift(drift, environment) {
|
|
|
3453
4089
|
}
|
|
3454
4090
|
return lines;
|
|
3455
4091
|
}
|
|
3456
|
-
var watchCommand =
|
|
4092
|
+
var watchCommand = defineCommand16({
|
|
3457
4093
|
meta: {
|
|
3458
4094
|
name: "watch",
|
|
3459
4095
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -3466,8 +4102,8 @@ var watchCommand = defineCommand14({
|
|
|
3466
4102
|
const handle = runWatch({
|
|
3467
4103
|
cwd: process.cwd(),
|
|
3468
4104
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
3469
|
-
onResult: (
|
|
3470
|
-
write(renderWatch(
|
|
4105
|
+
onResult: (result2) => {
|
|
4106
|
+
write(renderWatch(result2));
|
|
3471
4107
|
},
|
|
3472
4108
|
// A failing cycle is reported and watching continues. `reportError`
|
|
3473
4109
|
// marks the process failed, which `validate` wants and a loop does not:
|
|
@@ -3481,10 +4117,10 @@ var watchCommand = defineCommand14({
|
|
|
3481
4117
|
}
|
|
3482
4118
|
});
|
|
3483
4119
|
write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
|
|
3484
|
-
await new Promise((
|
|
4120
|
+
await new Promise((resolve7) => {
|
|
3485
4121
|
process.once("SIGINT", () => {
|
|
3486
4122
|
handle.close();
|
|
3487
|
-
|
|
4123
|
+
resolve7();
|
|
3488
4124
|
});
|
|
3489
4125
|
});
|
|
3490
4126
|
});
|
|
@@ -3492,7 +4128,7 @@ var watchCommand = defineCommand14({
|
|
|
3492
4128
|
});
|
|
3493
4129
|
|
|
3494
4130
|
// src/index.ts
|
|
3495
|
-
var main =
|
|
4131
|
+
var main = defineCommand17({
|
|
3496
4132
|
meta: {
|
|
3497
4133
|
name: "penv",
|
|
3498
4134
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -3504,7 +4140,9 @@ var main = defineCommand15({
|
|
|
3504
4140
|
get: getCommand,
|
|
3505
4141
|
set: setCommand,
|
|
3506
4142
|
mv: mvCommand,
|
|
4143
|
+
pull: pullCommand,
|
|
3507
4144
|
push: pushCommand,
|
|
4145
|
+
rotate: rotateCommand,
|
|
3508
4146
|
remove: removeCommand,
|
|
3509
4147
|
list: listCommand,
|
|
3510
4148
|
encrypt: encryptCommand,
|
|
@@ -3527,7 +4165,9 @@ export {
|
|
|
3527
4165
|
main,
|
|
3528
4166
|
renderDoctor,
|
|
3529
4167
|
renderMove,
|
|
4168
|
+
renderPull,
|
|
3530
4169
|
renderPush,
|
|
4170
|
+
renderRotate,
|
|
3531
4171
|
renderWatch,
|
|
3532
4172
|
runDecrypt,
|
|
3533
4173
|
runDoctor,
|
|
@@ -3539,8 +4179,10 @@ export {
|
|
|
3539
4179
|
runList,
|
|
3540
4180
|
runMain,
|
|
3541
4181
|
runMove,
|
|
4182
|
+
runPull,
|
|
3542
4183
|
runPush,
|
|
3543
4184
|
runRemove,
|
|
4185
|
+
runRotate,
|
|
3544
4186
|
runSet,
|
|
3545
4187
|
runValidate,
|
|
3546
4188
|
runWatch
|