@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.cjs
CHANGED
|
@@ -27,7 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
main: () => main,
|
|
28
28
|
renderDoctor: () => renderDoctor,
|
|
29
29
|
renderMove: () => renderMove,
|
|
30
|
+
renderPull: () => renderPull,
|
|
30
31
|
renderPush: () => renderPush,
|
|
32
|
+
renderRotate: () => renderRotate,
|
|
31
33
|
renderWatch: () => renderWatch,
|
|
32
34
|
runDecrypt: () => runDecrypt,
|
|
33
35
|
runDoctor: () => runDoctor,
|
|
@@ -39,15 +41,17 @@ __export(index_exports, {
|
|
|
39
41
|
runList: () => runList,
|
|
40
42
|
runMain: () => runMain,
|
|
41
43
|
runMove: () => runMove,
|
|
44
|
+
runPull: () => runPull,
|
|
42
45
|
runPush: () => runPush,
|
|
43
46
|
runRemove: () => runRemove,
|
|
47
|
+
runRotate: () => runRotate,
|
|
44
48
|
runSet: () => runSet,
|
|
45
49
|
runValidate: () => runValidate,
|
|
46
50
|
runWatch: () => runWatch
|
|
47
51
|
});
|
|
48
52
|
module.exports = __toCommonJS(index_exports);
|
|
49
|
-
var
|
|
50
|
-
var
|
|
53
|
+
var import_core21 = require("@penvhq/core");
|
|
54
|
+
var import_citty17 = require("citty");
|
|
51
55
|
|
|
52
56
|
// src/commands/doctor.ts
|
|
53
57
|
var import_core7 = require("@penvhq/core");
|
|
@@ -55,17 +59,35 @@ var import_sink_github2 = require("@penvhq/sink-github");
|
|
|
55
59
|
var import_citty3 = require("citty");
|
|
56
60
|
|
|
57
61
|
// src/project.ts
|
|
58
|
-
var
|
|
62
|
+
var import_node_path2 = require("path");
|
|
59
63
|
var import_core2 = require("@penvhq/core");
|
|
60
64
|
var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
|
|
61
65
|
|
|
62
66
|
// src/registry.ts
|
|
67
|
+
var import_node_module = require("module");
|
|
68
|
+
var import_node_path = require("path");
|
|
69
|
+
var import_node_url = require("url");
|
|
63
70
|
var import_core = require("@penvhq/core");
|
|
64
71
|
var import_provider_filesystem = require("@penvhq/provider-filesystem");
|
|
72
|
+
var import_provider_mock = require("@penvhq/provider-mock");
|
|
73
|
+
var import_provider_vault = require("@penvhq/provider-vault");
|
|
74
|
+
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
65
75
|
var LOCAL_TREE_TYPE = "filesystem";
|
|
66
76
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
67
|
-
[LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })]
|
|
77
|
+
[LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })],
|
|
78
|
+
["vault", ({ providerConfig }) => (0, import_provider_vault.createVaultProvider)({ path: providerConfig?.path ?? "penv" })],
|
|
79
|
+
["mock", ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })]
|
|
68
80
|
]);
|
|
81
|
+
var CONTRACT_METHODS = [
|
|
82
|
+
"read",
|
|
83
|
+
"write",
|
|
84
|
+
"list",
|
|
85
|
+
"remove",
|
|
86
|
+
"readMeta",
|
|
87
|
+
"writeMeta",
|
|
88
|
+
"removeMeta"
|
|
89
|
+
];
|
|
90
|
+
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
69
91
|
function isProviderRegistered(type) {
|
|
70
92
|
return REGISTRY.has(type);
|
|
71
93
|
}
|
|
@@ -76,20 +98,94 @@ function createProvider(type, context) {
|
|
|
76
98
|
}
|
|
77
99
|
return factory(context);
|
|
78
100
|
}
|
|
79
|
-
function
|
|
101
|
+
async function createSourceProvider(type, context) {
|
|
102
|
+
if (REGISTRY.has(type)) {
|
|
103
|
+
return createProvider(type, context);
|
|
104
|
+
}
|
|
105
|
+
return loadPluginProvider(type, context);
|
|
106
|
+
}
|
|
107
|
+
async function loadPluginProvider(type, context) {
|
|
108
|
+
const fromDir = resolutionBase(context);
|
|
109
|
+
const specifier = pluginSpecifier(context.providerConfig, type);
|
|
110
|
+
const resolved = resolvePlugin(specifier, fromDir);
|
|
111
|
+
if (resolved === void 0) {
|
|
112
|
+
throw unknownProvider(type, context.environment, specifier);
|
|
113
|
+
}
|
|
114
|
+
let mod;
|
|
115
|
+
try {
|
|
116
|
+
mod = await importPlugin(resolved);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new import_core.PenvError(
|
|
119
|
+
"PROVIDER_PLUGIN_LOAD",
|
|
120
|
+
`The provider package \`${specifier}\` for type \`${type}\` failed to load`,
|
|
121
|
+
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
125
|
+
if (typeof factory !== "function") {
|
|
126
|
+
throw new import_core.PenvError(
|
|
127
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
128
|
+
`\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
129
|
+
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const provider = await factory(context);
|
|
133
|
+
assertSatisfiesContract(provider, specifier);
|
|
134
|
+
return provider;
|
|
135
|
+
}
|
|
136
|
+
function assertProvidersRegistered(config, projectRoot) {
|
|
80
137
|
for (const [environment, provider] of Object.entries(config.providers)) {
|
|
81
|
-
if (
|
|
82
|
-
|
|
138
|
+
if (isProviderRegistered(provider.type)) {
|
|
139
|
+
continue;
|
|
83
140
|
}
|
|
141
|
+
const specifier = pluginSpecifier(provider, provider.type);
|
|
142
|
+
if (resolvePlugin(specifier, projectRoot) === void 0) {
|
|
143
|
+
throw unknownProvider(provider.type, environment, specifier);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function pluginSpecifier(providerConfig, type) {
|
|
148
|
+
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
149
|
+
}
|
|
150
|
+
function resolutionBase(context) {
|
|
151
|
+
return (0, import_node_path.dirname)(context.root);
|
|
152
|
+
}
|
|
153
|
+
function resolvePlugin(specifier, fromDir) {
|
|
154
|
+
try {
|
|
155
|
+
const require2 = (0, import_node_module.createRequire)((0, import_node_path.resolve)(fromDir, "noop.js"));
|
|
156
|
+
return require2.resolve(specifier);
|
|
157
|
+
} catch {
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function importPlugin(resolvedPath) {
|
|
162
|
+
const cached2 = pluginModuleCache.get(resolvedPath);
|
|
163
|
+
if (cached2 !== void 0) {
|
|
164
|
+
return cached2;
|
|
84
165
|
}
|
|
166
|
+
const loading = import((0, import_node_url.pathToFileURL)(resolvedPath).href);
|
|
167
|
+
pluginModuleCache.set(resolvedPath, loading);
|
|
168
|
+
return loading;
|
|
85
169
|
}
|
|
86
|
-
function
|
|
170
|
+
function assertSatisfiesContract(provider, specifier) {
|
|
171
|
+
for (const method of CONTRACT_METHODS) {
|
|
172
|
+
if (typeof provider[method] !== "function") {
|
|
173
|
+
throw new import_core.PenvError(
|
|
174
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
175
|
+
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
176
|
+
"It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function unknownProvider(type, environment, specifier) {
|
|
87
182
|
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
88
183
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
184
|
+
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}.`;
|
|
89
185
|
return new import_core.PenvError(
|
|
90
186
|
"UNKNOWN_PROVIDER",
|
|
91
187
|
`The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
|
|
92
|
-
|
|
188
|
+
remedy
|
|
93
189
|
);
|
|
94
190
|
}
|
|
95
191
|
|
|
@@ -97,9 +193,9 @@ function unknownProvider(type, environment) {
|
|
|
97
193
|
var PENV_DIR = ".penv";
|
|
98
194
|
function openProject(cwd) {
|
|
99
195
|
const { config, file } = (0, import_core2.loadConfig)(cwd);
|
|
100
|
-
const root = (0,
|
|
101
|
-
const penvDir = (0,
|
|
102
|
-
assertProvidersRegistered(config);
|
|
196
|
+
const root = (0, import_node_path2.dirname)(file);
|
|
197
|
+
const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
|
|
198
|
+
assertProvidersRegistered(config, root);
|
|
103
199
|
return {
|
|
104
200
|
root,
|
|
105
201
|
configFile: file,
|
|
@@ -118,6 +214,18 @@ function localTree(project) {
|
|
|
118
214
|
}
|
|
119
215
|
return project.provider;
|
|
120
216
|
}
|
|
217
|
+
async function sourceProviderFor(project, environment) {
|
|
218
|
+
const providerConfig = project.config.providers[environment];
|
|
219
|
+
if (providerConfig === void 0) {
|
|
220
|
+
return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
|
|
221
|
+
}
|
|
222
|
+
return createSourceProvider(providerConfig.type, {
|
|
223
|
+
root: project.penvDir,
|
|
224
|
+
config: project.config,
|
|
225
|
+
providerConfig,
|
|
226
|
+
environment
|
|
227
|
+
});
|
|
228
|
+
}
|
|
121
229
|
function targetEnvironment(project, explicit) {
|
|
122
230
|
return (0, import_core2.resolveEnvironment)(project.config, explicit);
|
|
123
231
|
}
|
|
@@ -493,36 +601,36 @@ async function runPush(options) {
|
|
|
493
601
|
decrypted: outbound.filter((item) => item.encrypted).length
|
|
494
602
|
};
|
|
495
603
|
}
|
|
496
|
-
function renderPush(
|
|
497
|
-
if (
|
|
604
|
+
function renderPush(result2) {
|
|
605
|
+
if (result2.pushed === 0) {
|
|
498
606
|
return formatRows([
|
|
499
607
|
{
|
|
500
608
|
glyph: CHECK,
|
|
501
609
|
label: "Nothing to push",
|
|
502
|
-
subject: `no values resolve for environment ${
|
|
610
|
+
subject: `no values resolve for environment ${result2.environment}`
|
|
503
611
|
}
|
|
504
612
|
]);
|
|
505
613
|
}
|
|
506
|
-
const target = `GitHub Actions for environment ${
|
|
614
|
+
const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
|
|
507
615
|
const rows = [
|
|
508
616
|
{
|
|
509
617
|
glyph: CHECK,
|
|
510
618
|
label: "Pushed",
|
|
511
|
-
subject: `${
|
|
619
|
+
subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
|
|
512
620
|
detail: `to ${target}`
|
|
513
621
|
},
|
|
514
622
|
{
|
|
515
623
|
glyph: CHECK,
|
|
516
624
|
label: "Scopes",
|
|
517
|
-
subject: `${
|
|
625
|
+
subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
|
|
518
626
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
519
627
|
}
|
|
520
628
|
];
|
|
521
|
-
if (
|
|
629
|
+
if (result2.decrypted > 0) {
|
|
522
630
|
rows.push({
|
|
523
631
|
glyph: WARN,
|
|
524
632
|
label: "Decrypted",
|
|
525
|
-
subject: `${
|
|
633
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
526
634
|
detail: "sent as plaintext for GitHub to re-seal under its own key"
|
|
527
635
|
});
|
|
528
636
|
}
|
|
@@ -542,19 +650,19 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
542
650
|
},
|
|
543
651
|
run({ args }) {
|
|
544
652
|
return guard(async () => {
|
|
545
|
-
const
|
|
653
|
+
const result2 = await runPush({
|
|
546
654
|
cwd: process.cwd(),
|
|
547
655
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
548
656
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
549
657
|
});
|
|
550
|
-
write(renderPush(
|
|
658
|
+
write(renderPush(result2));
|
|
551
659
|
});
|
|
552
660
|
}
|
|
553
661
|
});
|
|
554
662
|
|
|
555
663
|
// src/commands/validate.ts
|
|
556
|
-
var
|
|
557
|
-
var
|
|
664
|
+
var import_node_path3 = require("path");
|
|
665
|
+
var import_node_url2 = require("url");
|
|
558
666
|
var import_core6 = require("@penvhq/core");
|
|
559
667
|
var import_citty2 = require("citty");
|
|
560
668
|
var import_jiti = require("jiti");
|
|
@@ -623,16 +731,16 @@ function validationIssuesOf(cause, schemaPath) {
|
|
|
623
731
|
}
|
|
624
732
|
var schemaLoads = Promise.resolve();
|
|
625
733
|
function exclusively(work) {
|
|
626
|
-
const
|
|
627
|
-
schemaLoads =
|
|
734
|
+
const result2 = schemaLoads.then(work, work);
|
|
735
|
+
schemaLoads = result2.then(
|
|
628
736
|
() => void 0,
|
|
629
737
|
() => void 0
|
|
630
738
|
);
|
|
631
|
-
return
|
|
739
|
+
return result2;
|
|
632
740
|
}
|
|
633
741
|
function loadSchema(project, environment) {
|
|
634
742
|
const schemaPath = (0, import_core6.schemaFileOf)(project.config);
|
|
635
|
-
const file = (0,
|
|
743
|
+
const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
|
|
636
744
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
637
745
|
}
|
|
638
746
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
@@ -734,9 +842,9 @@ async function runValidate(options) {
|
|
|
734
842
|
}
|
|
735
843
|
}
|
|
736
844
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
737
|
-
const
|
|
738
|
-
if (!
|
|
739
|
-
for (const problem of
|
|
845
|
+
const result2 = schema.safeParse(object);
|
|
846
|
+
if (!result2.success) {
|
|
847
|
+
for (const problem of result2.error.issues) {
|
|
740
848
|
const path = problem.path.join(".");
|
|
741
849
|
if (undecryptable.has(path)) {
|
|
742
850
|
continue;
|
|
@@ -752,24 +860,24 @@ async function runValidate(options) {
|
|
|
752
860
|
}
|
|
753
861
|
return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
|
|
754
862
|
}
|
|
755
|
-
function renderValidate(
|
|
756
|
-
if (
|
|
863
|
+
function renderValidate(result2) {
|
|
864
|
+
if (result2.ok) {
|
|
757
865
|
return formatRows([
|
|
758
866
|
{
|
|
759
867
|
glyph: CHECK,
|
|
760
868
|
label: "Schema valid",
|
|
761
|
-
subject: `${
|
|
869
|
+
subject: `${result2.parameters} parameters for environment ${result2.environment}`
|
|
762
870
|
}
|
|
763
871
|
]);
|
|
764
872
|
}
|
|
765
|
-
const rows =
|
|
873
|
+
const rows = result2.issues.map((issue) => ({
|
|
766
874
|
glyph: WARN,
|
|
767
875
|
label: LABELS[issue.kind],
|
|
768
876
|
subject: issue.subject,
|
|
769
877
|
detail: issue.message
|
|
770
878
|
}));
|
|
771
879
|
const lines = formatRows(rows);
|
|
772
|
-
const remedies = [...new Set(
|
|
880
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
|
|
773
881
|
for (const remedy of remedies) {
|
|
774
882
|
if (remedy !== void 0) {
|
|
775
883
|
lines.push(` ${remedy}`);
|
|
@@ -787,12 +895,12 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
787
895
|
},
|
|
788
896
|
run({ args }) {
|
|
789
897
|
return guard(async () => {
|
|
790
|
-
const
|
|
898
|
+
const result2 = await runValidate({
|
|
791
899
|
cwd: process.cwd(),
|
|
792
900
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
793
901
|
});
|
|
794
|
-
write(renderValidate(
|
|
795
|
-
if (!
|
|
902
|
+
write(renderValidate(result2));
|
|
903
|
+
if (!result2.ok) {
|
|
796
904
|
process.exitCode = 1;
|
|
797
905
|
}
|
|
798
906
|
});
|
|
@@ -800,6 +908,7 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
800
908
|
});
|
|
801
909
|
|
|
802
910
|
// src/commands/doctor.ts
|
|
911
|
+
var STUCK_THRESHOLD_MS = 864e5;
|
|
803
912
|
function skipped(check, label) {
|
|
804
913
|
return {
|
|
805
914
|
check,
|
|
@@ -813,6 +922,8 @@ async function runDoctor(options) {
|
|
|
813
922
|
const project = openProject(options.cwd);
|
|
814
923
|
const environment = targetEnvironment(project, options.environment);
|
|
815
924
|
const findings = [];
|
|
925
|
+
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
926
|
+
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
816
927
|
const { schema, issues } = await loadSchema(project, environment);
|
|
817
928
|
if (schema !== void 0) {
|
|
818
929
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -861,6 +972,14 @@ async function runDoctor(options) {
|
|
|
861
972
|
findings.push(...plaintextSecretFindings(subjects, environment));
|
|
862
973
|
findings.push(...publicSecretFindings(subjects, environment, project.config));
|
|
863
974
|
findings.push(...encryptionFindings(subjects, environment));
|
|
975
|
+
const rotation = await rotationSubjects(project, environment, subjects, options.source);
|
|
976
|
+
if (rotation.kind === "unreachable") {
|
|
977
|
+
findings.push(rotation.finding);
|
|
978
|
+
} else {
|
|
979
|
+
findings.push(...overdueFindings(rotation.subjects, environment, now));
|
|
980
|
+
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
981
|
+
}
|
|
982
|
+
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
864
983
|
findings.push(...await sinkFindings(project, environment, options.sink));
|
|
865
984
|
findings.push({
|
|
866
985
|
check: "provider",
|
|
@@ -1278,6 +1397,246 @@ async function sinkFindings(project, environment, override) {
|
|
|
1278
1397
|
});
|
|
1279
1398
|
return findings;
|
|
1280
1399
|
}
|
|
1400
|
+
function humanizeMs(ms) {
|
|
1401
|
+
const abs = Math.max(0, ms);
|
|
1402
|
+
const day = 864e5;
|
|
1403
|
+
const hour = 36e5;
|
|
1404
|
+
const minute = 6e4;
|
|
1405
|
+
const round = (value, unit) => {
|
|
1406
|
+
const n = Math.round(value);
|
|
1407
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
1408
|
+
};
|
|
1409
|
+
if (abs >= day) return round(abs / day, "day");
|
|
1410
|
+
if (abs >= hour) return round(abs / hour, "hour");
|
|
1411
|
+
if (abs >= minute) return round(abs / minute, "minute");
|
|
1412
|
+
return round(abs / 1e3, "second");
|
|
1413
|
+
}
|
|
1414
|
+
function refPathOf(ref) {
|
|
1415
|
+
return [...ref.namespace, ref.name].join("/");
|
|
1416
|
+
}
|
|
1417
|
+
async function rotationSubjects(project, environment, local, override) {
|
|
1418
|
+
const providerConfig = project.config.providers[environment];
|
|
1419
|
+
if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
|
|
1420
|
+
return { kind: "read", subjects: local };
|
|
1421
|
+
}
|
|
1422
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1423
|
+
try {
|
|
1424
|
+
const subjects = await Promise.all(
|
|
1425
|
+
local.map(async ({ resolution }) => ({
|
|
1426
|
+
resolution,
|
|
1427
|
+
meta: await source.readMeta(resolution.ref)
|
|
1428
|
+
}))
|
|
1429
|
+
);
|
|
1430
|
+
return { kind: "read", subjects };
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
return {
|
|
1433
|
+
kind: "unreachable",
|
|
1434
|
+
finding: {
|
|
1435
|
+
check: "rotation-overdue",
|
|
1436
|
+
severity: "unknown",
|
|
1437
|
+
label: "Rotation",
|
|
1438
|
+
subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
|
|
1439
|
+
detail: errorDetail(error)
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function overdueFindings(subjects, environment, now) {
|
|
1445
|
+
const findings = [];
|
|
1446
|
+
for (const { resolution, meta } of subjects) {
|
|
1447
|
+
const { policy, lastRotated } = (0, import_core7.rotationOf)(meta, environment);
|
|
1448
|
+
if (policy === void 0 || lastRotated === null) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const interval = (0, import_core7.tryParseDuration)(policy);
|
|
1452
|
+
if (interval === void 0) {
|
|
1453
|
+
findings.push({
|
|
1454
|
+
check: "rotation-overdue",
|
|
1455
|
+
severity: "warning",
|
|
1456
|
+
label: "Rotation policy invalid",
|
|
1457
|
+
subject: resolution.parameter,
|
|
1458
|
+
detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
|
|
1459
|
+
});
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
const last = Date.parse(lastRotated);
|
|
1463
|
+
if (Number.isNaN(last)) {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const overdueBy = now.getTime() - last - interval;
|
|
1467
|
+
if (overdueBy <= 0) {
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
findings.push({
|
|
1471
|
+
check: "rotation-overdue",
|
|
1472
|
+
severity: "warning",
|
|
1473
|
+
label: "Rotation overdue",
|
|
1474
|
+
subject: resolution.parameter,
|
|
1475
|
+
detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
|
|
1476
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
if (findings.length > 0) {
|
|
1480
|
+
return findings;
|
|
1481
|
+
}
|
|
1482
|
+
return [
|
|
1483
|
+
{
|
|
1484
|
+
check: "rotation-overdue",
|
|
1485
|
+
severity: "pass",
|
|
1486
|
+
label: "Rotation freshness",
|
|
1487
|
+
subject: `no parameter is past its rotation policy for ${environment}`
|
|
1488
|
+
}
|
|
1489
|
+
];
|
|
1490
|
+
}
|
|
1491
|
+
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1492
|
+
const findings = [];
|
|
1493
|
+
for (const { resolution, meta } of subjects) {
|
|
1494
|
+
if (!(0, import_core7.isStuck)(meta, environment, now, stuckThresholdMs)) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const { rotatingSince } = (0, import_core7.rotationOf)(meta, environment);
|
|
1498
|
+
if (rotatingSince === null) {
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
const openFor = now.getTime() - Date.parse(rotatingSince);
|
|
1502
|
+
findings.push({
|
|
1503
|
+
check: "rotation-stuck",
|
|
1504
|
+
severity: "warning",
|
|
1505
|
+
label: "Rotation stuck",
|
|
1506
|
+
subject: resolution.parameter,
|
|
1507
|
+
detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
|
|
1508
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (findings.length > 0) {
|
|
1512
|
+
return findings;
|
|
1513
|
+
}
|
|
1514
|
+
return [
|
|
1515
|
+
{
|
|
1516
|
+
check: "rotation-stuck",
|
|
1517
|
+
severity: "pass",
|
|
1518
|
+
label: "Rotation progress",
|
|
1519
|
+
subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
|
|
1520
|
+
}
|
|
1521
|
+
];
|
|
1522
|
+
}
|
|
1523
|
+
function driftKey(file) {
|
|
1524
|
+
return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
|
|
1525
|
+
}
|
|
1526
|
+
function scopeKey(scope) {
|
|
1527
|
+
switch (scope.kind) {
|
|
1528
|
+
case "unscoped":
|
|
1529
|
+
return "unscoped";
|
|
1530
|
+
case "environment":
|
|
1531
|
+
return `environment:${scope.environment}`;
|
|
1532
|
+
case "local":
|
|
1533
|
+
return "local";
|
|
1534
|
+
case "environment-local":
|
|
1535
|
+
return `environment-local:${scope.environment}`;
|
|
1536
|
+
default:
|
|
1537
|
+
return (0, import_core7.assertNever)(scope, "scope");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function relevantToEnvironment(file, environment) {
|
|
1541
|
+
const scope = file.scope;
|
|
1542
|
+
if (scope.kind === "unscoped") return true;
|
|
1543
|
+
if (scope.kind === "environment") return scope.environment === environment;
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
async function readRelevant(provider, environment) {
|
|
1547
|
+
const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
|
|
1548
|
+
const stored = await Promise.all(files.map((file) => provider.read(file)));
|
|
1549
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const [index, file] of files.entries()) {
|
|
1551
|
+
const value = stored[index];
|
|
1552
|
+
if (value !== void 0) {
|
|
1553
|
+
entries.set(driftKey(file), { file, stored: value });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return entries;
|
|
1557
|
+
}
|
|
1558
|
+
async function providerDriftFindings(project, environment, override) {
|
|
1559
|
+
const providerConfig = project.config.providers[environment];
|
|
1560
|
+
if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
|
|
1561
|
+
return [
|
|
1562
|
+
{
|
|
1563
|
+
check: "provider-value-drift",
|
|
1564
|
+
severity: "pass",
|
|
1565
|
+
label: "Provider values",
|
|
1566
|
+
subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
|
|
1567
|
+
}
|
|
1568
|
+
];
|
|
1569
|
+
}
|
|
1570
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1571
|
+
let local;
|
|
1572
|
+
let remote;
|
|
1573
|
+
try {
|
|
1574
|
+
local = await readRelevant(project.provider, environment);
|
|
1575
|
+
remote = await readRelevant(source, environment);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
return [
|
|
1578
|
+
{
|
|
1579
|
+
check: "provider-value-drift",
|
|
1580
|
+
severity: "unknown",
|
|
1581
|
+
label: "Provider values",
|
|
1582
|
+
subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
|
|
1583
|
+
detail: errorDetail(error)
|
|
1584
|
+
}
|
|
1585
|
+
];
|
|
1586
|
+
}
|
|
1587
|
+
const keys = keySourceFor(project, environment);
|
|
1588
|
+
const findings = [];
|
|
1589
|
+
const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
|
|
1590
|
+
for (const address of addresses) {
|
|
1591
|
+
const here = local.get(address);
|
|
1592
|
+
const there = remote.get(address);
|
|
1593
|
+
if (here !== void 0 && there !== void 0) {
|
|
1594
|
+
const opened = (0, import_core7.openValue)(here.file, here.stored, keys);
|
|
1595
|
+
if (opened.kind !== "plaintext") {
|
|
1596
|
+
findings.push({
|
|
1597
|
+
check: "provider-value-drift",
|
|
1598
|
+
severity: "unknown",
|
|
1599
|
+
label: "Provider value unreadable",
|
|
1600
|
+
subject: (0, import_core7.formatValueFile)(here.file),
|
|
1601
|
+
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1602
|
+
});
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (opened.value !== there.stored) {
|
|
1606
|
+
findings.push({
|
|
1607
|
+
check: "provider-value-drift",
|
|
1608
|
+
severity: "failure",
|
|
1609
|
+
label: "Provider value drift",
|
|
1610
|
+
subject: (0, import_core7.formatValueFile)(here.file),
|
|
1611
|
+
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const present = here ?? there;
|
|
1617
|
+
if (present === void 0) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
findings.push({
|
|
1621
|
+
check: "provider-value-drift",
|
|
1622
|
+
severity: "warning",
|
|
1623
|
+
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1624
|
+
subject: (0, import_core7.formatValueFile)(present.file),
|
|
1625
|
+
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`
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (findings.length > 0) {
|
|
1629
|
+
return findings;
|
|
1630
|
+
}
|
|
1631
|
+
return [
|
|
1632
|
+
{
|
|
1633
|
+
check: "provider-value-drift",
|
|
1634
|
+
severity: "pass",
|
|
1635
|
+
label: "Provider values",
|
|
1636
|
+
subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
|
|
1637
|
+
}
|
|
1638
|
+
];
|
|
1639
|
+
}
|
|
1281
1640
|
function renderDoctor(report) {
|
|
1282
1641
|
const rows = report.findings.map((finding) => ({
|
|
1283
1642
|
glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
|
|
@@ -1364,32 +1723,44 @@ function sealFor(project, file, value, parameter, environment) {
|
|
|
1364
1723
|
}
|
|
1365
1724
|
return (0, import_core8.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1366
1725
|
}
|
|
1367
|
-
async function
|
|
1368
|
-
const project =
|
|
1369
|
-
const
|
|
1370
|
-
const scope = targetScope(project, options, options.key);
|
|
1371
|
-
const environment = policyEnvironment(project, options);
|
|
1372
|
-
const secret = (0, import_core8.isSecret)(await project.provider.readMeta(ref), environment);
|
|
1726
|
+
async function sealAwareWrite(options) {
|
|
1727
|
+
const { project, provider, ref, scope, value, environment } = options;
|
|
1728
|
+
const secret = (0, import_core8.isSecret)(await provider.readMeta(ref), environment);
|
|
1373
1729
|
const file = {
|
|
1374
1730
|
namespace: ref.namespace,
|
|
1375
1731
|
name: ref.name,
|
|
1376
1732
|
scope,
|
|
1377
1733
|
encrypted: secret
|
|
1378
1734
|
};
|
|
1379
|
-
const stored = secret ? sealFor(project, file,
|
|
1380
|
-
await
|
|
1381
|
-
await
|
|
1382
|
-
return {
|
|
1735
|
+
const stored = secret ? sealFor(project, file, value, (0, import_core8.parameterId)(ref), environment) : value;
|
|
1736
|
+
await provider.write(file, stored);
|
|
1737
|
+
await provider.remove({ ...file, encrypted: !secret });
|
|
1738
|
+
return { encrypted: secret, location: (0, import_core8.formatValueFile)(file) };
|
|
1383
1739
|
}
|
|
1384
|
-
function
|
|
1740
|
+
async function runSet(options) {
|
|
1741
|
+
const project = openProject(options.cwd);
|
|
1742
|
+
const ref = refFromKey(options.key, project.config);
|
|
1743
|
+
const scope = targetScope(project, options, options.key);
|
|
1744
|
+
const environment = policyEnvironment(project, options);
|
|
1745
|
+
const { encrypted, location } = await sealAwareWrite({
|
|
1746
|
+
project,
|
|
1747
|
+
provider: project.provider,
|
|
1748
|
+
ref,
|
|
1749
|
+
scope,
|
|
1750
|
+
value: options.value,
|
|
1751
|
+
environment
|
|
1752
|
+
});
|
|
1753
|
+
return { parameter: options.key, location, encrypted };
|
|
1754
|
+
}
|
|
1755
|
+
function renderSet(result2) {
|
|
1385
1756
|
return formatRows([
|
|
1386
1757
|
{
|
|
1387
1758
|
glyph: CHECK,
|
|
1388
1759
|
label: "Wrote",
|
|
1389
|
-
subject: `${PENV_DIR}/${
|
|
1760
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1390
1761
|
// The `.enc` suffix says this already, but only to a reader who knows the
|
|
1391
1762
|
// grammar. Meta decided it, not the command line, so the command says so.
|
|
1392
|
-
...
|
|
1763
|
+
...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
1393
1764
|
}
|
|
1394
1765
|
]);
|
|
1395
1766
|
}
|
|
@@ -1525,13 +1896,13 @@ var UndecryptableAt = class extends import_core9.PenvError {
|
|
|
1525
1896
|
);
|
|
1526
1897
|
}
|
|
1527
1898
|
};
|
|
1528
|
-
function renderReseal(
|
|
1899
|
+
function renderReseal(result2, verb) {
|
|
1529
1900
|
return formatRows([
|
|
1530
1901
|
{
|
|
1531
1902
|
glyph: CHECK,
|
|
1532
1903
|
label: verb,
|
|
1533
|
-
subject: `${PENV_DIR}/${
|
|
1534
|
-
detail: `${PENV_DIR}/${
|
|
1904
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1905
|
+
detail: `${PENV_DIR}/${result2.removed} removed`
|
|
1535
1906
|
}
|
|
1536
1907
|
]);
|
|
1537
1908
|
}
|
|
@@ -1554,8 +1925,8 @@ var encryptCommand = (0, import_citty5.defineCommand)({
|
|
|
1554
1925
|
args: SCOPE_ARGS,
|
|
1555
1926
|
run({ args }) {
|
|
1556
1927
|
return guard(async () => {
|
|
1557
|
-
const
|
|
1558
|
-
write(renderReseal(
|
|
1928
|
+
const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1929
|
+
write(renderReseal(result2, "Encrypted"));
|
|
1559
1930
|
});
|
|
1560
1931
|
}
|
|
1561
1932
|
});
|
|
@@ -1564,15 +1935,15 @@ var decryptCommand = (0, import_citty5.defineCommand)({
|
|
|
1564
1935
|
args: SCOPE_ARGS,
|
|
1565
1936
|
run({ args }) {
|
|
1566
1937
|
return guard(async () => {
|
|
1567
|
-
const
|
|
1568
|
-
write(renderReseal(
|
|
1938
|
+
const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1939
|
+
write(renderReseal(result2, "Decrypted"));
|
|
1569
1940
|
});
|
|
1570
1941
|
}
|
|
1571
1942
|
});
|
|
1572
1943
|
|
|
1573
1944
|
// src/commands/generate.ts
|
|
1574
1945
|
var import_node_fs = require("fs");
|
|
1575
|
-
var
|
|
1946
|
+
var import_node_path4 = require("path");
|
|
1576
1947
|
var import_core10 = require("@penvhq/core");
|
|
1577
1948
|
var import_citty6 = require("citty");
|
|
1578
1949
|
var DEFAULT_OUTPUT = ".env";
|
|
@@ -1624,28 +1995,28 @@ function runGenerate(options) {
|
|
|
1624
1995
|
const project = openProject(options.cwd);
|
|
1625
1996
|
const environment = targetEnvironment(project, options.environment);
|
|
1626
1997
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
1627
|
-
const file = options.out === void 0 ? (0,
|
|
1998
|
+
const file = options.out === void 0 ? (0, import_node_path4.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path4.isAbsolute)(options.out) ? options.out : (0, import_node_path4.resolve)(options.cwd, options.out);
|
|
1628
1999
|
(0, import_node_fs.writeFileSync)(file, (0, import_core10.serializeDotenv)(entries), "utf8");
|
|
1629
2000
|
return { file, environment, entries: entries.length, decrypted };
|
|
1630
2001
|
}
|
|
1631
2002
|
function displayPath(cwd, file) {
|
|
1632
|
-
const rel = (0,
|
|
2003
|
+
const rel = (0, import_node_path4.relative)(cwd, file);
|
|
1633
2004
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
1634
2005
|
}
|
|
1635
|
-
function renderGenerate(
|
|
2006
|
+
function renderGenerate(result2, cwd) {
|
|
1636
2007
|
const rows = [
|
|
1637
2008
|
{
|
|
1638
2009
|
glyph: CHECK,
|
|
1639
2010
|
label: "Generated",
|
|
1640
|
-
subject: displayPath(cwd,
|
|
1641
|
-
detail: `${
|
|
2011
|
+
subject: displayPath(cwd, result2.file),
|
|
2012
|
+
detail: `${result2.entries} variables for environment ${result2.environment}`
|
|
1642
2013
|
}
|
|
1643
2014
|
];
|
|
1644
|
-
if (
|
|
2015
|
+
if (result2.decrypted > 0) {
|
|
1645
2016
|
rows.push({
|
|
1646
2017
|
glyph: WARN,
|
|
1647
2018
|
label: "Decrypted",
|
|
1648
|
-
subject: `${
|
|
2019
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
1649
2020
|
detail: "written as plaintext into the artifact"
|
|
1650
2021
|
});
|
|
1651
2022
|
}
|
|
@@ -1664,13 +2035,13 @@ var generateCommand = (0, import_citty6.defineCommand)({
|
|
|
1664
2035
|
run({ args }) {
|
|
1665
2036
|
return guard(async () => {
|
|
1666
2037
|
const cwd = process.cwd();
|
|
1667
|
-
const
|
|
2038
|
+
const result2 = runGenerate({
|
|
1668
2039
|
cwd,
|
|
1669
2040
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
1670
2041
|
...args.out === void 0 ? {} : { out: args.out },
|
|
1671
2042
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1672
2043
|
});
|
|
1673
|
-
write(renderGenerate(
|
|
2044
|
+
write(renderGenerate(result2, cwd));
|
|
1674
2045
|
});
|
|
1675
2046
|
}
|
|
1676
2047
|
});
|
|
@@ -1761,13 +2132,13 @@ var getCommand = (0, import_citty7.defineCommand)({
|
|
|
1761
2132
|
|
|
1762
2133
|
// src/commands/import.ts
|
|
1763
2134
|
var import_node_fs4 = require("fs");
|
|
1764
|
-
var
|
|
2135
|
+
var import_node_path7 = require("path");
|
|
1765
2136
|
var import_core14 = require("@penvhq/core");
|
|
1766
2137
|
var import_citty9 = require("citty");
|
|
1767
2138
|
|
|
1768
2139
|
// src/detect.ts
|
|
1769
2140
|
var import_node_fs2 = require("fs");
|
|
1770
|
-
var
|
|
2141
|
+
var import_node_path5 = require("path");
|
|
1771
2142
|
var import_core12 = require("@penvhq/core");
|
|
1772
2143
|
var SIGNATURES = [
|
|
1773
2144
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
@@ -1793,11 +2164,11 @@ function exportsSchema(file) {
|
|
|
1793
2164
|
);
|
|
1794
2165
|
}
|
|
1795
2166
|
function occupied(cwd, relative3) {
|
|
1796
|
-
const file = (0,
|
|
2167
|
+
const file = (0, import_node_path5.join)(cwd, ...relative3.split("/"));
|
|
1797
2168
|
return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
|
|
1798
2169
|
}
|
|
1799
2170
|
function schemaFileFor(cwd) {
|
|
1800
|
-
const dir = (0, import_node_fs2.existsSync)((0,
|
|
2171
|
+
const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
|
|
1801
2172
|
const preferred = `${dir}env.ts`;
|
|
1802
2173
|
if (!occupied(cwd, preferred)) {
|
|
1803
2174
|
return { file: preferred };
|
|
@@ -1825,7 +2196,7 @@ function dependenciesOf(cwd) {
|
|
|
1825
2196
|
return names;
|
|
1826
2197
|
}
|
|
1827
2198
|
function manifestOf(cwd) {
|
|
1828
|
-
const file = (0,
|
|
2199
|
+
const file = (0, import_node_path5.join)(cwd, "package.json");
|
|
1829
2200
|
if (!(0, import_node_fs2.existsSync)(file)) {
|
|
1830
2201
|
return void 0;
|
|
1831
2202
|
}
|
|
@@ -1868,7 +2239,7 @@ function hasImportsBlock(cwd) {
|
|
|
1868
2239
|
|
|
1869
2240
|
// src/commands/init.ts
|
|
1870
2241
|
var import_node_fs3 = require("fs");
|
|
1871
|
-
var
|
|
2242
|
+
var import_node_path6 = require("path");
|
|
1872
2243
|
var import_promises = require("readline/promises");
|
|
1873
2244
|
var import_core13 = require("@penvhq/core");
|
|
1874
2245
|
var import_citty8 = require("citty");
|
|
@@ -1933,7 +2304,7 @@ function configOf(decisions) {
|
|
|
1933
2304
|
return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
|
|
1934
2305
|
}
|
|
1935
2306
|
function declaredIn(root) {
|
|
1936
|
-
const file = (0,
|
|
2307
|
+
const file = (0, import_node_path6.join)(root, CONFIG_FILE);
|
|
1937
2308
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
1938
2309
|
return void 0;
|
|
1939
2310
|
}
|
|
@@ -2329,7 +2700,7 @@ function renderTsconfig(target, alias) {
|
|
|
2329
2700
|
`;
|
|
2330
2701
|
}
|
|
2331
2702
|
function ensurePenvDir(root) {
|
|
2332
|
-
const dir = (0,
|
|
2703
|
+
const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
|
|
2333
2704
|
if ((0, import_node_fs3.existsSync)(dir)) {
|
|
2334
2705
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
2335
2706
|
}
|
|
@@ -2337,7 +2708,7 @@ function ensurePenvDir(root) {
|
|
|
2337
2708
|
return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
|
|
2338
2709
|
}
|
|
2339
2710
|
function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
2340
|
-
const file = (0,
|
|
2711
|
+
const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
|
|
2341
2712
|
if ((0, import_node_fs3.existsSync)(file)) {
|
|
2342
2713
|
return {
|
|
2343
2714
|
target: "schema",
|
|
@@ -2346,7 +2717,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2346
2717
|
note: "(yours \u2014 penv never regenerates it)"
|
|
2347
2718
|
};
|
|
2348
2719
|
}
|
|
2349
|
-
(0, import_node_fs3.mkdirSync)((0,
|
|
2720
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
2350
2721
|
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
|
|
2351
2722
|
return {
|
|
2352
2723
|
target: "schema",
|
|
@@ -2356,7 +2727,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2356
2727
|
};
|
|
2357
2728
|
}
|
|
2358
2729
|
function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
2359
|
-
const file = (0,
|
|
2730
|
+
const file = (0, import_node_path6.join)(root, CONFIG_FILE);
|
|
2360
2731
|
if ((0, import_node_fs3.existsSync)(file)) {
|
|
2361
2732
|
return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
|
|
2362
2733
|
}
|
|
@@ -2366,7 +2737,7 @@ function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
|
2366
2737
|
function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
2367
2738
|
const alias = decisions.alias;
|
|
2368
2739
|
const imports = alias.startsWith(IMPORTS_PREFIX);
|
|
2369
|
-
const file = (0,
|
|
2740
|
+
const file = (0, import_node_path6.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
|
|
2370
2741
|
const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
|
|
2371
2742
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
2372
2743
|
if (imports) {
|
|
@@ -2409,14 +2780,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
2409
2780
|
};
|
|
2410
2781
|
}
|
|
2411
2782
|
function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
2412
|
-
const file = (0,
|
|
2783
|
+
const file = (0, import_node_path6.join)(root, PENV_DIR2, GITIGNORE_FILE);
|
|
2413
2784
|
const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
|
|
2414
2785
|
const wanted = renderGitignore(decisions);
|
|
2415
2786
|
const existing = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : void 0;
|
|
2416
2787
|
if (existing === wanted) {
|
|
2417
2788
|
return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
|
|
2418
2789
|
}
|
|
2419
|
-
(0, import_node_fs3.mkdirSync)((0,
|
|
2790
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
|
|
2420
2791
|
(0, import_node_fs3.writeFileSync)(file, wanted, "utf8");
|
|
2421
2792
|
return {
|
|
2422
2793
|
target: "gitignore",
|
|
@@ -2434,20 +2805,20 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2434
2805
|
];
|
|
2435
2806
|
}
|
|
2436
2807
|
function runInit(options) {
|
|
2437
|
-
const root = (0,
|
|
2808
|
+
const root = (0, import_node_path6.resolve)(options.cwd);
|
|
2438
2809
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
2439
2810
|
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
2440
2811
|
}
|
|
2441
|
-
function renderInit(
|
|
2442
|
-
const steps =
|
|
2812
|
+
function renderInit(result2) {
|
|
2813
|
+
const steps = result2.steps.map((step) => {
|
|
2443
2814
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
2444
2815
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
2445
2816
|
});
|
|
2446
2817
|
return [
|
|
2447
2818
|
...formatSteps(steps),
|
|
2448
2819
|
"",
|
|
2449
|
-
`Done. Declare your parameters in ${
|
|
2450
|
-
...
|
|
2820
|
+
`Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
|
|
2821
|
+
...result2.decisions.environments.length === 0 ? [
|
|
2451
2822
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
2452
2823
|
] : []
|
|
2453
2824
|
];
|
|
@@ -2486,7 +2857,7 @@ var initCommand = (0, import_citty8.defineCommand)({
|
|
|
2486
2857
|
},
|
|
2487
2858
|
run({ args }) {
|
|
2488
2859
|
return guard(async () => {
|
|
2489
|
-
const root = (0,
|
|
2860
|
+
const root = (0, import_node_path6.resolve)(process.cwd());
|
|
2490
2861
|
const environments = environmentsFromFlag(args.env);
|
|
2491
2862
|
const plan2 = planInit(root, {
|
|
2492
2863
|
...args.schema === void 0 ? {} : { schema: args.schema },
|
|
@@ -2577,7 +2948,7 @@ function assertDeclared(segment, config) {
|
|
|
2577
2948
|
throw new import_core14.UnknownEnvironmentError(segment, config.environments);
|
|
2578
2949
|
}
|
|
2579
2950
|
function scopeFromFilename(file, config) {
|
|
2580
|
-
const name = (0,
|
|
2951
|
+
const name = (0, import_node_path7.basename)(file);
|
|
2581
2952
|
const segments = name.split(".");
|
|
2582
2953
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
2583
2954
|
if (start === -1) {
|
|
@@ -2661,7 +3032,7 @@ function environmentNamed(file, explicit) {
|
|
|
2661
3032
|
if (explicit !== void 0 && explicit.trim().length > 0) {
|
|
2662
3033
|
return explicit.trim();
|
|
2663
3034
|
}
|
|
2664
|
-
const segments = (0,
|
|
3035
|
+
const segments = (0, import_node_path7.basename)(file).split(".");
|
|
2665
3036
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
2666
3037
|
if (start === -1) {
|
|
2667
3038
|
return void 0;
|
|
@@ -2692,8 +3063,8 @@ function configInEffect(cwd, environment) {
|
|
|
2692
3063
|
return { config: openProject(cwd).config, decisions };
|
|
2693
3064
|
}
|
|
2694
3065
|
function importDotenv(options) {
|
|
2695
|
-
const cwd = (0,
|
|
2696
|
-
const file = (0,
|
|
3066
|
+
const cwd = (0, import_node_path7.resolve)(options.cwd);
|
|
3067
|
+
const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
|
|
2697
3068
|
if (!(0, import_node_fs4.existsSync)(file)) {
|
|
2698
3069
|
throw new import_core14.PenvError(
|
|
2699
3070
|
"IMPORT_FILE_MISSING",
|
|
@@ -2752,7 +3123,7 @@ function importDotenv(options) {
|
|
|
2752
3123
|
};
|
|
2753
3124
|
}
|
|
2754
3125
|
function displayPath2(root, file) {
|
|
2755
|
-
const rel = (0,
|
|
3126
|
+
const rel = (0, import_node_path7.relative)(root, file);
|
|
2756
3127
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
2757
3128
|
}
|
|
2758
3129
|
function keptSchemaStep(step, variables) {
|
|
@@ -2776,19 +3147,19 @@ function skippedValidationStep(environments) {
|
|
|
2776
3147
|
note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
|
|
2777
3148
|
};
|
|
2778
3149
|
}
|
|
2779
|
-
function renderImport(
|
|
2780
|
-
const steps = [{ glyph: CHECK, text: `Found ${
|
|
2781
|
-
if (
|
|
2782
|
-
const plural =
|
|
3150
|
+
function renderImport(result2, validation) {
|
|
3151
|
+
const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
|
|
3152
|
+
if (result2.orphanComments > 0) {
|
|
3153
|
+
const plural = result2.orphanComments === 1 ? "comment" : "comments";
|
|
2783
3154
|
steps.push({
|
|
2784
3155
|
glyph: WARN,
|
|
2785
|
-
text: `Dropped ${
|
|
3156
|
+
text: `Dropped ${result2.orphanComments} orphan ${plural}`,
|
|
2786
3157
|
note: "attached to no variable, so nothing to describe"
|
|
2787
3158
|
});
|
|
2788
3159
|
}
|
|
2789
|
-
for (const step of
|
|
3160
|
+
for (const step of result2.steps) {
|
|
2790
3161
|
if (step.target === "schema" && step.action === "kept") {
|
|
2791
|
-
steps.push(keptSchemaStep(step,
|
|
3162
|
+
steps.push(keptSchemaStep(step, result2.variables));
|
|
2792
3163
|
continue;
|
|
2793
3164
|
}
|
|
2794
3165
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
@@ -2796,10 +3167,10 @@ function renderImport(result, validation) {
|
|
|
2796
3167
|
step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
|
|
2797
3168
|
);
|
|
2798
3169
|
}
|
|
2799
|
-
steps.push({ glyph: CHECK, text: `Created ${displayPath2(
|
|
3170
|
+
steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
|
|
2800
3171
|
const lines = formatSteps(steps);
|
|
2801
3172
|
if (validation === void 0) {
|
|
2802
|
-
lines.push(...formatSteps([skippedValidationStep(
|
|
3173
|
+
lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
|
|
2803
3174
|
} else if (validation.ok) {
|
|
2804
3175
|
lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
|
|
2805
3176
|
} else {
|
|
@@ -2844,12 +3215,12 @@ var import_core15 = require("@penvhq/core");
|
|
|
2844
3215
|
var import_citty10 = require("citty");
|
|
2845
3216
|
|
|
2846
3217
|
// src/keychain.ts
|
|
2847
|
-
var
|
|
3218
|
+
var import_node_module2 = require("module");
|
|
2848
3219
|
var import_meta = {};
|
|
2849
3220
|
var cached;
|
|
2850
3221
|
function entryConstructor() {
|
|
2851
3222
|
if (cached === void 0) {
|
|
2852
|
-
const require2 = (0,
|
|
3223
|
+
const require2 = (0, import_node_module2.createRequire)(import_meta.url);
|
|
2853
3224
|
cached = require2("@napi-rs/keyring").Entry;
|
|
2854
3225
|
}
|
|
2855
3226
|
return cached;
|
|
@@ -2913,19 +3284,19 @@ function runKeyCreate(options) {
|
|
|
2913
3284
|
key
|
|
2914
3285
|
};
|
|
2915
3286
|
}
|
|
2916
|
-
function renderKeyCreate(
|
|
2917
|
-
if (
|
|
3287
|
+
function renderKeyCreate(result2) {
|
|
3288
|
+
if (result2.source === "keychain") {
|
|
2918
3289
|
return [
|
|
2919
|
-
`A new key for environment ${
|
|
3290
|
+
`A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
2920
3291
|
"",
|
|
2921
3292
|
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
|
|
2922
3293
|
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
2923
3294
|
];
|
|
2924
3295
|
}
|
|
2925
3296
|
return [
|
|
2926
|
-
`A new key for environment ${
|
|
3297
|
+
`A new key for environment ${result2.environment}. penv did not store it.`,
|
|
2927
3298
|
"",
|
|
2928
|
-
` ${
|
|
3299
|
+
` ${result2.variable}=${result2.key}`,
|
|
2929
3300
|
"",
|
|
2930
3301
|
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
|
|
2931
3302
|
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
|
|
@@ -2997,11 +3368,11 @@ async function runList(options) {
|
|
|
2997
3368
|
}
|
|
2998
3369
|
return { environment, parameters };
|
|
2999
3370
|
}
|
|
3000
|
-
function renderList(
|
|
3001
|
-
if (
|
|
3002
|
-
return [`No parameters in ${PENV_DIR}/ for environment ${
|
|
3371
|
+
function renderList(result2) {
|
|
3372
|
+
if (result2.parameters.length === 0) {
|
|
3373
|
+
return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
|
|
3003
3374
|
}
|
|
3004
|
-
return columns(
|
|
3375
|
+
return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
|
|
3005
3376
|
}
|
|
3006
3377
|
var listCommand = (0, import_citty11.defineCommand)({
|
|
3007
3378
|
meta: { name: "list", description: "List parameters" },
|
|
@@ -3011,11 +3382,11 @@ var listCommand = (0, import_citty11.defineCommand)({
|
|
|
3011
3382
|
},
|
|
3012
3383
|
run({ args }) {
|
|
3013
3384
|
return guard(async () => {
|
|
3014
|
-
const
|
|
3385
|
+
const result2 = await runList({
|
|
3015
3386
|
cwd: process.cwd(),
|
|
3016
3387
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
3017
3388
|
});
|
|
3018
|
-
write(args.json === true ? [JSON.stringify(
|
|
3389
|
+
write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
|
|
3019
3390
|
});
|
|
3020
3391
|
}
|
|
3021
3392
|
});
|
|
@@ -3127,20 +3498,20 @@ async function runMove(options) {
|
|
|
3127
3498
|
schema: { was: (0, import_core17.accessPath)(from).join("."), now: (0, import_core17.accessPath)(to).join(".") }
|
|
3128
3499
|
};
|
|
3129
3500
|
}
|
|
3130
|
-
function renderMove(
|
|
3131
|
-
const rows =
|
|
3501
|
+
function renderMove(result2) {
|
|
3502
|
+
const rows = result2.files.map((file) => ({
|
|
3132
3503
|
glyph: CHECK,
|
|
3133
3504
|
label: "Moved",
|
|
3134
3505
|
subject: `${PENV_DIR}/${file.to}`,
|
|
3135
3506
|
...file.resealed ? { detail: "re-sealed for its new address" } : {}
|
|
3136
3507
|
}));
|
|
3137
|
-
if (
|
|
3138
|
-
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${
|
|
3508
|
+
if (result2.meta !== void 0) {
|
|
3509
|
+
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
|
|
3139
3510
|
}
|
|
3140
3511
|
const lines = formatRows(rows);
|
|
3141
3512
|
lines.push(
|
|
3142
3513
|
"",
|
|
3143
|
-
` .penv/env.ts still declares \`${
|
|
3514
|
+
` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
|
|
3144
3515
|
" or `penv validate` will report the value as unused and the declaration as unset."
|
|
3145
3516
|
);
|
|
3146
3517
|
return lines;
|
|
@@ -3166,9 +3537,86 @@ var mvCommand = (0, import_citty12.defineCommand)({
|
|
|
3166
3537
|
}
|
|
3167
3538
|
});
|
|
3168
3539
|
|
|
3540
|
+
// src/commands/pull.ts
|
|
3541
|
+
var import_citty13 = require("citty");
|
|
3542
|
+
async function runPull(options) {
|
|
3543
|
+
const project = openProject(options.cwd);
|
|
3544
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3545
|
+
const source = await sourceProviderFor(project, environment);
|
|
3546
|
+
if (source.type === LOCAL_TREE_TYPE) {
|
|
3547
|
+
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3548
|
+
}
|
|
3549
|
+
const tree = localTree(project);
|
|
3550
|
+
const files = await source.list();
|
|
3551
|
+
let values = 0;
|
|
3552
|
+
for (const file of files) {
|
|
3553
|
+
const value = await source.read(file);
|
|
3554
|
+
if (value === void 0) {
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
tree.writeSync(file, value);
|
|
3558
|
+
values += 1;
|
|
3559
|
+
}
|
|
3560
|
+
const refs = refsFrom(files);
|
|
3561
|
+
let meta = 0;
|
|
3562
|
+
for (const ref of refs) {
|
|
3563
|
+
const block = await source.readMeta(ref);
|
|
3564
|
+
if (block === void 0) {
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
tree.writeMetaSync(ref, block);
|
|
3568
|
+
meta += 1;
|
|
3569
|
+
}
|
|
3570
|
+
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
3571
|
+
}
|
|
3572
|
+
function renderPull(result2) {
|
|
3573
|
+
if (result2.localSource) {
|
|
3574
|
+
return formatRows([
|
|
3575
|
+
{
|
|
3576
|
+
glyph: CHECK,
|
|
3577
|
+
label: "Nothing to pull",
|
|
3578
|
+
subject: `environment ${result2.environment} has no separate source of truth`,
|
|
3579
|
+
detail: "its values live in the local .penv tree already"
|
|
3580
|
+
}
|
|
3581
|
+
]);
|
|
3582
|
+
}
|
|
3583
|
+
return formatRows([
|
|
3584
|
+
{
|
|
3585
|
+
glyph: CHECK,
|
|
3586
|
+
label: "Pulled",
|
|
3587
|
+
subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
|
|
3588
|
+
detail: `from the ${result2.source} provider for environment ${result2.environment}`
|
|
3589
|
+
},
|
|
3590
|
+
{
|
|
3591
|
+
glyph: CHECK,
|
|
3592
|
+
label: "Parameters",
|
|
3593
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
|
|
3594
|
+
detail: "written into the local .penv tree"
|
|
3595
|
+
}
|
|
3596
|
+
]);
|
|
3597
|
+
}
|
|
3598
|
+
var pullCommand = (0, import_citty13.defineCommand)({
|
|
3599
|
+
meta: {
|
|
3600
|
+
name: "pull",
|
|
3601
|
+
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3602
|
+
},
|
|
3603
|
+
args: {
|
|
3604
|
+
env: { type: "string", description: "The environment to pull" }
|
|
3605
|
+
},
|
|
3606
|
+
run({ args }) {
|
|
3607
|
+
return guard(async () => {
|
|
3608
|
+
const result2 = await runPull({
|
|
3609
|
+
cwd: process.cwd(),
|
|
3610
|
+
...args.env === void 0 ? {} : { environment: args.env }
|
|
3611
|
+
});
|
|
3612
|
+
write(renderPull(result2));
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3169
3617
|
// src/commands/remove.ts
|
|
3170
3618
|
var import_core18 = require("@penvhq/core");
|
|
3171
|
-
var
|
|
3619
|
+
var import_citty14 = require("citty");
|
|
3172
3620
|
async function runRemove(options) {
|
|
3173
3621
|
const project = openProject(options.cwd);
|
|
3174
3622
|
const ref = refFromKey(options.key);
|
|
@@ -3193,9 +3641,9 @@ async function runRemove(options) {
|
|
|
3193
3641
|
considered: files.map((file) => (0, import_core18.formatValueFile)(file))
|
|
3194
3642
|
};
|
|
3195
3643
|
}
|
|
3196
|
-
function renderRemove(
|
|
3197
|
-
if (
|
|
3198
|
-
const first =
|
|
3644
|
+
function renderRemove(result2) {
|
|
3645
|
+
if (result2.removed.length === 0) {
|
|
3646
|
+
const first = result2.considered[0] ?? result2.parameter;
|
|
3199
3647
|
return formatRows([
|
|
3200
3648
|
{
|
|
3201
3649
|
glyph: WARN,
|
|
@@ -3205,14 +3653,14 @@ function renderRemove(result) {
|
|
|
3205
3653
|
}
|
|
3206
3654
|
]);
|
|
3207
3655
|
}
|
|
3208
|
-
const rows =
|
|
3656
|
+
const rows = result2.removed.map((location) => ({
|
|
3209
3657
|
glyph: CHECK,
|
|
3210
3658
|
label: "Removed",
|
|
3211
3659
|
subject: `${PENV_DIR}/${location}`
|
|
3212
3660
|
}));
|
|
3213
3661
|
return formatRows(rows);
|
|
3214
3662
|
}
|
|
3215
|
-
var removeCommand = (0,
|
|
3663
|
+
var removeCommand = (0, import_citty14.defineCommand)({
|
|
3216
3664
|
meta: { name: "remove", description: "Delete a parameter" },
|
|
3217
3665
|
args: {
|
|
3218
3666
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -3238,15 +3686,195 @@ var removeCommand = (0, import_citty13.defineCommand)({
|
|
|
3238
3686
|
}
|
|
3239
3687
|
});
|
|
3240
3688
|
|
|
3689
|
+
// src/commands/rotate.ts
|
|
3690
|
+
var import_core19 = require("@penvhq/core");
|
|
3691
|
+
var import_citty15 = require("citty");
|
|
3692
|
+
function rotatingFile(ref, environment) {
|
|
3693
|
+
return {
|
|
3694
|
+
namespace: ref.namespace,
|
|
3695
|
+
name: ref.name,
|
|
3696
|
+
scope: { kind: "environment", environment },
|
|
3697
|
+
encrypted: false
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
3701
|
+
if (provider.type === LOCAL_TREE_TYPE) {
|
|
3702
|
+
await sealAwareWrite({
|
|
3703
|
+
project,
|
|
3704
|
+
provider,
|
|
3705
|
+
ref,
|
|
3706
|
+
scope: { kind: "environment", environment },
|
|
3707
|
+
value,
|
|
3708
|
+
environment
|
|
3709
|
+
});
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
await provider.write(rotatingFile(ref, environment), value);
|
|
3713
|
+
}
|
|
3714
|
+
function requireNewValue(value, phase, key) {
|
|
3715
|
+
if (value === void 0) {
|
|
3716
|
+
throw new import_core19.PenvError(
|
|
3717
|
+
"ROTATION_NO_VALUE",
|
|
3718
|
+
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3719
|
+
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
return value;
|
|
3723
|
+
}
|
|
3724
|
+
function requireRetaining(provider, environment) {
|
|
3725
|
+
if (!(0, import_core19.retainsPrevious)(provider)) {
|
|
3726
|
+
throw new import_core19.PenvError(
|
|
3727
|
+
"ROTATION_NOT_RETAINING",
|
|
3728
|
+
`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`,
|
|
3729
|
+
"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."
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
return provider;
|
|
3733
|
+
}
|
|
3734
|
+
async function runRotate(options) {
|
|
3735
|
+
const project = openProject(options.cwd);
|
|
3736
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3737
|
+
const ref = refFromKey(options.key, project.config);
|
|
3738
|
+
const provider = await sourceProviderFor(project, environment);
|
|
3739
|
+
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3740
|
+
const before = await provider.readMeta(ref);
|
|
3741
|
+
const { mechanism } = (0, import_core19.rotationOf)(before, environment);
|
|
3742
|
+
if (mechanism === void 0) {
|
|
3743
|
+
throw new import_core19.PenvError(
|
|
3744
|
+
"ROTATION_NO_MECHANISM",
|
|
3745
|
+
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3746
|
+
'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.'
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
const begin = options.begin === true;
|
|
3750
|
+
const complete = options.complete === true;
|
|
3751
|
+
if (mechanism === "atomic-cutover") {
|
|
3752
|
+
if (begin || complete) {
|
|
3753
|
+
throw new import_core19.PenvError(
|
|
3754
|
+
"ROTATION_MECHANISM_MISMATCH",
|
|
3755
|
+
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3756
|
+
"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."
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3759
|
+
const value = requireNewValue(options.value, "cutover", options.key);
|
|
3760
|
+
await writeRotatedValue(project, provider, ref, environment, value);
|
|
3761
|
+
const after2 = (0, import_core19.completeRotation)(before, environment, nowIso);
|
|
3762
|
+
await provider.writeMeta(ref, after2);
|
|
3763
|
+
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
3764
|
+
}
|
|
3765
|
+
const retaining = requireRetaining(provider, environment);
|
|
3766
|
+
if (begin === complete) {
|
|
3767
|
+
throw new import_core19.PenvError(
|
|
3768
|
+
"ROTATION_PHASE_REQUIRED",
|
|
3769
|
+
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3770
|
+
"`--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."
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
if (begin) {
|
|
3774
|
+
const value = requireNewValue(options.value, "begin", options.key);
|
|
3775
|
+
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
3776
|
+
const after2 = (0, import_core19.beginRotation)(before, environment, nowIso);
|
|
3777
|
+
await retaining.writeMeta(ref, after2);
|
|
3778
|
+
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
3779
|
+
}
|
|
3780
|
+
const after = (0, import_core19.completeRotation)(before, environment, nowIso);
|
|
3781
|
+
await retaining.writeMeta(ref, after);
|
|
3782
|
+
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
3783
|
+
}
|
|
3784
|
+
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
3785
|
+
const { state, rotatingSince, lastRotated } = (0, import_core19.rotationOf)(after, environment);
|
|
3786
|
+
return {
|
|
3787
|
+
parameter: [...ref.namespace, ref.name].join("/"),
|
|
3788
|
+
environment,
|
|
3789
|
+
mechanism,
|
|
3790
|
+
phase,
|
|
3791
|
+
source,
|
|
3792
|
+
wroteValue,
|
|
3793
|
+
state,
|
|
3794
|
+
rotatingSince,
|
|
3795
|
+
lastRotated
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
function renderRotate(result2) {
|
|
3799
|
+
if (result2.phase === "begin") {
|
|
3800
|
+
return formatRows([
|
|
3801
|
+
{
|
|
3802
|
+
glyph: CHECK,
|
|
3803
|
+
label: "Rotating",
|
|
3804
|
+
subject: result2.parameter,
|
|
3805
|
+
detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
|
|
3806
|
+
},
|
|
3807
|
+
{
|
|
3808
|
+
glyph: CHECK,
|
|
3809
|
+
label: "Previous",
|
|
3810
|
+
subject: "still readable",
|
|
3811
|
+
detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
|
|
3812
|
+
}
|
|
3813
|
+
]);
|
|
3814
|
+
}
|
|
3815
|
+
if (result2.phase === "complete") {
|
|
3816
|
+
return formatRows([
|
|
3817
|
+
{
|
|
3818
|
+
glyph: CHECK,
|
|
3819
|
+
label: "Rotated",
|
|
3820
|
+
subject: result2.parameter,
|
|
3821
|
+
detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3822
|
+
}
|
|
3823
|
+
]);
|
|
3824
|
+
}
|
|
3825
|
+
return formatRows([
|
|
3826
|
+
{
|
|
3827
|
+
glyph: CHECK,
|
|
3828
|
+
label: "Rotated",
|
|
3829
|
+
subject: result2.parameter,
|
|
3830
|
+
detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3831
|
+
}
|
|
3832
|
+
]);
|
|
3833
|
+
}
|
|
3834
|
+
var rotateCommand = (0, import_citty15.defineCommand)({
|
|
3835
|
+
meta: {
|
|
3836
|
+
name: "rotate",
|
|
3837
|
+
description: "Rotate a parameter's value by the mechanism its meta declares"
|
|
3838
|
+
},
|
|
3839
|
+
args: {
|
|
3840
|
+
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
3841
|
+
value: {
|
|
3842
|
+
type: "positional",
|
|
3843
|
+
required: false,
|
|
3844
|
+
description: "The new value; read from stdin if omitted. Not needed with --complete"
|
|
3845
|
+
},
|
|
3846
|
+
env: { type: "string", description: "The environment to rotate in" },
|
|
3847
|
+
begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
|
|
3848
|
+
complete: { type: "boolean", description: "Close a dual-valid grace window" }
|
|
3849
|
+
},
|
|
3850
|
+
run({ args }) {
|
|
3851
|
+
return guard(async () => {
|
|
3852
|
+
const value = args.complete === true ? void 0 : args.value ?? await readStdin();
|
|
3853
|
+
write(
|
|
3854
|
+
renderRotate(
|
|
3855
|
+
await runRotate({
|
|
3856
|
+
cwd: process.cwd(),
|
|
3857
|
+
key: args.key,
|
|
3858
|
+
...value === void 0 ? {} : { value },
|
|
3859
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
3860
|
+
...args.begin === void 0 ? {} : { begin: args.begin },
|
|
3861
|
+
...args.complete === void 0 ? {} : { complete: args.complete }
|
|
3862
|
+
})
|
|
3863
|
+
)
|
|
3864
|
+
);
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
});
|
|
3868
|
+
|
|
3241
3869
|
// src/commands/watch.ts
|
|
3242
3870
|
var import_node_fs5 = require("fs");
|
|
3243
|
-
var
|
|
3244
|
-
var
|
|
3245
|
-
var
|
|
3871
|
+
var import_node_path8 = require("path");
|
|
3872
|
+
var import_core20 = require("@penvhq/core");
|
|
3873
|
+
var import_citty16 = require("citty");
|
|
3246
3874
|
var DEBOUNCE_MS = 100;
|
|
3247
3875
|
function runWatch(options) {
|
|
3248
3876
|
const project = openProject(options.cwd);
|
|
3249
|
-
const configFile = (0,
|
|
3877
|
+
const configFile = (0, import_node_path8.basename)(project.configFile);
|
|
3250
3878
|
const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
|
|
3251
3879
|
const watchers = /* @__PURE__ */ new Set();
|
|
3252
3880
|
let timer;
|
|
@@ -3263,12 +3891,12 @@ function runWatch(options) {
|
|
|
3263
3891
|
}
|
|
3264
3892
|
running = true;
|
|
3265
3893
|
try {
|
|
3266
|
-
const
|
|
3894
|
+
const result2 = await runValidate({
|
|
3267
3895
|
cwd: options.cwd,
|
|
3268
3896
|
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
3269
3897
|
});
|
|
3270
3898
|
if (!closed) {
|
|
3271
|
-
options.onResult?.(
|
|
3899
|
+
options.onResult?.(result2);
|
|
3272
3900
|
}
|
|
3273
3901
|
} catch (error) {
|
|
3274
3902
|
if (!closed) {
|
|
@@ -3299,8 +3927,8 @@ function runWatch(options) {
|
|
|
3299
3927
|
watcher.close();
|
|
3300
3928
|
}
|
|
3301
3929
|
function armRecovery(target, recursive, only) {
|
|
3302
|
-
const parent = (0,
|
|
3303
|
-
const name = (0,
|
|
3930
|
+
const parent = (0, import_node_path8.dirname)(target);
|
|
3931
|
+
const name = (0, import_node_path8.basename)(target);
|
|
3304
3932
|
let recovery;
|
|
3305
3933
|
try {
|
|
3306
3934
|
recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
|
|
@@ -3311,7 +3939,7 @@ function runWatch(options) {
|
|
|
3311
3939
|
stop(recovery);
|
|
3312
3940
|
return;
|
|
3313
3941
|
}
|
|
3314
|
-
if (filename !== null && (0,
|
|
3942
|
+
if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
|
|
3315
3943
|
return;
|
|
3316
3944
|
}
|
|
3317
3945
|
if (!(0, import_node_fs5.existsSync)(target)) {
|
|
@@ -3346,7 +3974,7 @@ function runWatch(options) {
|
|
|
3346
3974
|
schedule();
|
|
3347
3975
|
return;
|
|
3348
3976
|
}
|
|
3349
|
-
if (only !== void 0 && (filename === null || (0,
|
|
3977
|
+
if (only !== void 0 && (filename === null || (0, import_node_path8.basename)(filename) !== only)) {
|
|
3350
3978
|
return;
|
|
3351
3979
|
}
|
|
3352
3980
|
schedule();
|
|
@@ -3373,10 +4001,10 @@ function runWatch(options) {
|
|
|
3373
4001
|
watchers.add(watcher);
|
|
3374
4002
|
}
|
|
3375
4003
|
addWatcher(project.penvDir, true);
|
|
3376
|
-
addWatcher((0,
|
|
3377
|
-
if ((0,
|
|
3378
|
-
const schemaFile = (0,
|
|
3379
|
-
addWatcher((0,
|
|
4004
|
+
addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
|
|
4005
|
+
if ((0, import_core20.schemaInsideTree)(project.config) === void 0) {
|
|
4006
|
+
const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core20.schemaFileOf)(project.config));
|
|
4007
|
+
addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
|
|
3380
4008
|
}
|
|
3381
4009
|
void validate();
|
|
3382
4010
|
return {
|
|
@@ -3392,8 +4020,8 @@ function runWatch(options) {
|
|
|
3392
4020
|
}
|
|
3393
4021
|
};
|
|
3394
4022
|
}
|
|
3395
|
-
function renderWatch(
|
|
3396
|
-
return ["", ...renderValidate(
|
|
4023
|
+
function renderWatch(result2) {
|
|
4024
|
+
return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
|
|
3397
4025
|
}
|
|
3398
4026
|
function renderDrift(drift, environment) {
|
|
3399
4027
|
const rows = [
|
|
@@ -3419,7 +4047,7 @@ function renderDrift(drift, environment) {
|
|
|
3419
4047
|
}
|
|
3420
4048
|
return lines;
|
|
3421
4049
|
}
|
|
3422
|
-
var watchCommand = (0,
|
|
4050
|
+
var watchCommand = (0, import_citty16.defineCommand)({
|
|
3423
4051
|
meta: {
|
|
3424
4052
|
name: "watch",
|
|
3425
4053
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -3432,8 +4060,8 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3432
4060
|
const handle = runWatch({
|
|
3433
4061
|
cwd: process.cwd(),
|
|
3434
4062
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
3435
|
-
onResult: (
|
|
3436
|
-
write(renderWatch(
|
|
4063
|
+
onResult: (result2) => {
|
|
4064
|
+
write(renderWatch(result2));
|
|
3437
4065
|
},
|
|
3438
4066
|
// A failing cycle is reported and watching continues. `reportError`
|
|
3439
4067
|
// marks the process failed, which `validate` wants and a loop does not:
|
|
@@ -3447,10 +4075,10 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3447
4075
|
}
|
|
3448
4076
|
});
|
|
3449
4077
|
write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
|
|
3450
|
-
await new Promise((
|
|
4078
|
+
await new Promise((resolve7) => {
|
|
3451
4079
|
process.once("SIGINT", () => {
|
|
3452
4080
|
handle.close();
|
|
3453
|
-
|
|
4081
|
+
resolve7();
|
|
3454
4082
|
});
|
|
3455
4083
|
});
|
|
3456
4084
|
});
|
|
@@ -3458,7 +4086,7 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3458
4086
|
});
|
|
3459
4087
|
|
|
3460
4088
|
// src/index.ts
|
|
3461
|
-
var main = (0,
|
|
4089
|
+
var main = (0, import_citty17.defineCommand)({
|
|
3462
4090
|
meta: {
|
|
3463
4091
|
name: "penv",
|
|
3464
4092
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -3470,7 +4098,9 @@ var main = (0, import_citty15.defineCommand)({
|
|
|
3470
4098
|
get: getCommand,
|
|
3471
4099
|
set: setCommand,
|
|
3472
4100
|
mv: mvCommand,
|
|
4101
|
+
pull: pullCommand,
|
|
3473
4102
|
push: pushCommand,
|
|
4103
|
+
rotate: rotateCommand,
|
|
3474
4104
|
remove: removeCommand,
|
|
3475
4105
|
list: listCommand,
|
|
3476
4106
|
encrypt: encryptCommand,
|
|
@@ -3482,8 +4112,8 @@ var main = (0, import_citty15.defineCommand)({
|
|
|
3482
4112
|
}
|
|
3483
4113
|
});
|
|
3484
4114
|
function runMain() {
|
|
3485
|
-
(0,
|
|
3486
|
-
return (0,
|
|
4115
|
+
(0, import_core21.setKeychain)(defaultKeychain);
|
|
4116
|
+
return (0, import_citty17.runMain)(main);
|
|
3487
4117
|
}
|
|
3488
4118
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3489
4119
|
0 && (module.exports = {
|
|
@@ -3494,7 +4124,9 @@ function runMain() {
|
|
|
3494
4124
|
main,
|
|
3495
4125
|
renderDoctor,
|
|
3496
4126
|
renderMove,
|
|
4127
|
+
renderPull,
|
|
3497
4128
|
renderPush,
|
|
4129
|
+
renderRotate,
|
|
3498
4130
|
renderWatch,
|
|
3499
4131
|
runDecrypt,
|
|
3500
4132
|
runDoctor,
|
|
@@ -3506,8 +4138,10 @@ function runMain() {
|
|
|
3506
4138
|
runList,
|
|
3507
4139
|
runMain,
|
|
3508
4140
|
runMove,
|
|
4141
|
+
runPull,
|
|
3509
4142
|
runPush,
|
|
3510
4143
|
runRemove,
|
|
4144
|
+
runRotate,
|
|
3511
4145
|
runSet,
|
|
3512
4146
|
runValidate,
|
|
3513
4147
|
runWatch
|