@penvhq/cli 0.1.0 → 0.3.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 +1042 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -7
- package/dist/index.d.ts +165 -7
- package/dist/index.js +986 -197
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
package/dist/index.cjs
CHANGED
|
@@ -26,28 +26,34 @@ __export(index_exports, {
|
|
|
26
26
|
insertEnvAlias: () => insertEnvAlias,
|
|
27
27
|
main: () => main,
|
|
28
28
|
renderDoctor: () => renderDoctor,
|
|
29
|
+
renderFill: () => renderFill,
|
|
29
30
|
renderMove: () => renderMove,
|
|
31
|
+
renderPull: () => renderPull,
|
|
30
32
|
renderPush: () => renderPush,
|
|
33
|
+
renderRotate: () => renderRotate,
|
|
31
34
|
renderWatch: () => renderWatch,
|
|
32
35
|
runDecrypt: () => runDecrypt,
|
|
33
36
|
runDoctor: () => runDoctor,
|
|
34
37
|
runEncrypt: () => runEncrypt,
|
|
35
38
|
runExplain: () => runExplain,
|
|
39
|
+
runFill: () => runFill,
|
|
36
40
|
runGenerate: () => runGenerate,
|
|
37
41
|
runGet: () => runGet,
|
|
38
42
|
runInit: () => runInit,
|
|
39
43
|
runList: () => runList,
|
|
40
44
|
runMain: () => runMain,
|
|
41
45
|
runMove: () => runMove,
|
|
46
|
+
runPull: () => runPull,
|
|
42
47
|
runPush: () => runPush,
|
|
43
48
|
runRemove: () => runRemove,
|
|
49
|
+
runRotate: () => runRotate,
|
|
44
50
|
runSet: () => runSet,
|
|
45
51
|
runValidate: () => runValidate,
|
|
46
52
|
runWatch: () => runWatch
|
|
47
53
|
});
|
|
48
54
|
module.exports = __toCommonJS(index_exports);
|
|
49
|
-
var
|
|
50
|
-
var
|
|
55
|
+
var import_core22 = require("@penvhq/core");
|
|
56
|
+
var import_citty18 = require("citty");
|
|
51
57
|
|
|
52
58
|
// src/commands/doctor.ts
|
|
53
59
|
var import_core7 = require("@penvhq/core");
|
|
@@ -55,17 +61,42 @@ var import_sink_github2 = require("@penvhq/sink-github");
|
|
|
55
61
|
var import_citty3 = require("citty");
|
|
56
62
|
|
|
57
63
|
// src/project.ts
|
|
58
|
-
var
|
|
64
|
+
var import_node_path2 = require("path");
|
|
59
65
|
var import_core2 = require("@penvhq/core");
|
|
60
66
|
var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
|
|
61
67
|
|
|
62
68
|
// src/registry.ts
|
|
69
|
+
var import_node_module = require("module");
|
|
70
|
+
var import_node_path = require("path");
|
|
71
|
+
var import_node_url = require("url");
|
|
63
72
|
var import_core = require("@penvhq/core");
|
|
64
73
|
var import_provider_filesystem = require("@penvhq/provider-filesystem");
|
|
74
|
+
var import_provider_kubernetes = require("@penvhq/provider-kubernetes");
|
|
75
|
+
var import_provider_mock = require("@penvhq/provider-mock");
|
|
76
|
+
var import_provider_ssm = require("@penvhq/provider-ssm");
|
|
77
|
+
var import_provider_vault = require("@penvhq/provider-vault");
|
|
78
|
+
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
65
79
|
var LOCAL_TREE_TYPE = "filesystem";
|
|
66
80
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
67
|
-
[LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })]
|
|
81
|
+
[LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })],
|
|
82
|
+
["vault", ({ providerConfig }) => (0, import_provider_vault.createVaultProvider)({ path: providerConfig?.path ?? "penv" })],
|
|
83
|
+
["ssm", ({ providerConfig }) => (0, import_provider_ssm.createSsmProvider)({ path: providerConfig?.path ?? "penv" })],
|
|
84
|
+
[
|
|
85
|
+
"kubernetes",
|
|
86
|
+
({ providerConfig }) => (0, import_provider_kubernetes.createKubernetesProvider)(kubernetesOptions(providerConfig))
|
|
87
|
+
],
|
|
88
|
+
["mock", ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })]
|
|
68
89
|
]);
|
|
90
|
+
var CONTRACT_METHODS = [
|
|
91
|
+
"read",
|
|
92
|
+
"write",
|
|
93
|
+
"list",
|
|
94
|
+
"remove",
|
|
95
|
+
"readMeta",
|
|
96
|
+
"writeMeta",
|
|
97
|
+
"removeMeta"
|
|
98
|
+
];
|
|
99
|
+
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
69
100
|
function isProviderRegistered(type) {
|
|
70
101
|
return REGISTRY.has(type);
|
|
71
102
|
}
|
|
@@ -76,20 +107,102 @@ function createProvider(type, context) {
|
|
|
76
107
|
}
|
|
77
108
|
return factory(context);
|
|
78
109
|
}
|
|
79
|
-
function
|
|
110
|
+
async function createSourceProvider(type, context) {
|
|
111
|
+
if (REGISTRY.has(type)) {
|
|
112
|
+
return createProvider(type, context);
|
|
113
|
+
}
|
|
114
|
+
return loadPluginProvider(type, context);
|
|
115
|
+
}
|
|
116
|
+
async function loadPluginProvider(type, context) {
|
|
117
|
+
const fromDir = resolutionBase(context);
|
|
118
|
+
const specifier = pluginSpecifier(context.providerConfig, type);
|
|
119
|
+
const resolved = resolvePlugin(specifier, fromDir);
|
|
120
|
+
if (resolved === void 0) {
|
|
121
|
+
throw unknownProvider(type, context.environment, specifier);
|
|
122
|
+
}
|
|
123
|
+
let mod;
|
|
124
|
+
try {
|
|
125
|
+
mod = await importPlugin(resolved);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new import_core.PenvError(
|
|
128
|
+
"PROVIDER_PLUGIN_LOAD",
|
|
129
|
+
`The provider package \`${specifier}\` for type \`${type}\` failed to load`,
|
|
130
|
+
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
134
|
+
if (typeof factory !== "function") {
|
|
135
|
+
throw new import_core.PenvError(
|
|
136
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
137
|
+
`\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
138
|
+
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const provider = await factory(context);
|
|
142
|
+
assertSatisfiesContract(provider, specifier);
|
|
143
|
+
return provider;
|
|
144
|
+
}
|
|
145
|
+
function assertProvidersRegistered(config, projectRoot) {
|
|
80
146
|
for (const [environment, provider] of Object.entries(config.providers)) {
|
|
81
|
-
if (
|
|
82
|
-
|
|
147
|
+
if (isProviderRegistered(provider.type)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const specifier = pluginSpecifier(provider, provider.type);
|
|
151
|
+
if (resolvePlugin(specifier, projectRoot) === void 0) {
|
|
152
|
+
throw unknownProvider(provider.type, environment, specifier);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function pluginSpecifier(providerConfig, type) {
|
|
157
|
+
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
158
|
+
}
|
|
159
|
+
function resolutionBase(context) {
|
|
160
|
+
return (0, import_node_path.dirname)(context.root);
|
|
161
|
+
}
|
|
162
|
+
function resolvePlugin(specifier, fromDir) {
|
|
163
|
+
try {
|
|
164
|
+
const require2 = (0, import_node_module.createRequire)((0, import_node_path.resolve)(fromDir, "noop.js"));
|
|
165
|
+
return require2.resolve(specifier);
|
|
166
|
+
} catch {
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function importPlugin(resolvedPath) {
|
|
171
|
+
const cached2 = pluginModuleCache.get(resolvedPath);
|
|
172
|
+
if (cached2 !== void 0) {
|
|
173
|
+
return cached2;
|
|
174
|
+
}
|
|
175
|
+
const loading = import((0, import_node_url.pathToFileURL)(resolvedPath).href);
|
|
176
|
+
pluginModuleCache.set(resolvedPath, loading);
|
|
177
|
+
return loading;
|
|
178
|
+
}
|
|
179
|
+
function assertSatisfiesContract(provider, specifier) {
|
|
180
|
+
for (const method of CONTRACT_METHODS) {
|
|
181
|
+
if (typeof provider[method] !== "function") {
|
|
182
|
+
throw new import_core.PenvError(
|
|
183
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
184
|
+
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
185
|
+
"It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
|
|
186
|
+
);
|
|
83
187
|
}
|
|
84
188
|
}
|
|
85
189
|
}
|
|
86
|
-
function
|
|
190
|
+
function kubernetesOptions(providerConfig) {
|
|
191
|
+
const path = providerConfig?.path ?? "penv";
|
|
192
|
+
const slash = path.indexOf("/");
|
|
193
|
+
if (slash === -1) return { secretName: path };
|
|
194
|
+
const namespace = path.slice(0, slash);
|
|
195
|
+
const secretName = path.slice(slash + 1) || "penv";
|
|
196
|
+
return namespace === "" ? { secretName } : { namespace, secretName };
|
|
197
|
+
}
|
|
198
|
+
function unknownProvider(type, environment, specifier) {
|
|
87
199
|
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
88
200
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
201
|
+
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
202
|
return new import_core.PenvError(
|
|
90
203
|
"UNKNOWN_PROVIDER",
|
|
91
204
|
`The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
|
|
92
|
-
|
|
205
|
+
remedy
|
|
93
206
|
);
|
|
94
207
|
}
|
|
95
208
|
|
|
@@ -97,9 +210,9 @@ function unknownProvider(type, environment) {
|
|
|
97
210
|
var PENV_DIR = ".penv";
|
|
98
211
|
function openProject(cwd) {
|
|
99
212
|
const { config, file } = (0, import_core2.loadConfig)(cwd);
|
|
100
|
-
const root = (0,
|
|
101
|
-
const penvDir = (0,
|
|
102
|
-
assertProvidersRegistered(config);
|
|
213
|
+
const root = (0, import_node_path2.dirname)(file);
|
|
214
|
+
const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
|
|
215
|
+
assertProvidersRegistered(config, root);
|
|
103
216
|
return {
|
|
104
217
|
root,
|
|
105
218
|
configFile: file,
|
|
@@ -118,6 +231,18 @@ function localTree(project) {
|
|
|
118
231
|
}
|
|
119
232
|
return project.provider;
|
|
120
233
|
}
|
|
234
|
+
async function sourceProviderFor(project, environment) {
|
|
235
|
+
const providerConfig = project.config.providers[environment];
|
|
236
|
+
if (providerConfig === void 0) {
|
|
237
|
+
return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
|
|
238
|
+
}
|
|
239
|
+
return createSourceProvider(providerConfig.type, {
|
|
240
|
+
root: project.penvDir,
|
|
241
|
+
config: project.config,
|
|
242
|
+
providerConfig,
|
|
243
|
+
environment
|
|
244
|
+
});
|
|
245
|
+
}
|
|
121
246
|
function targetEnvironment(project, explicit) {
|
|
122
247
|
return (0, import_core2.resolveEnvironment)(project.config, explicit);
|
|
123
248
|
}
|
|
@@ -138,6 +263,26 @@ function refFromKey(key, config) {
|
|
|
138
263
|
}
|
|
139
264
|
return { namespace: segments.slice(0, -1), name };
|
|
140
265
|
}
|
|
266
|
+
function assertWritableKey(key) {
|
|
267
|
+
const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
|
|
268
|
+
if (segments.length === 0 || segments.every((s) => (0, import_core2.isCanonicalSegment)(s))) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const ref = (0, import_core2.refFromAccessPath)(segments);
|
|
272
|
+
if (ref !== void 0) {
|
|
273
|
+
const suggestion = [...ref.namespace, ref.name].join("/");
|
|
274
|
+
throw new import_core2.PenvError(
|
|
275
|
+
"PARAMETER_KEY_CASING",
|
|
276
|
+
`\`${key}\` is not a canonical parameter name`,
|
|
277
|
+
`Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
throw new import_core2.PenvError(
|
|
281
|
+
"PARAMETER_KEY_UNREACHABLE",
|
|
282
|
+
`No value file can be named that reaches \`${key}\``,
|
|
283
|
+
"Parameter files are lower-case and hyphenated, and this key maps to no such file \u2014 a run of capitals like `apiURL` cannot be reached (use `api-url`, which the schema reads as `apiUrl`). Run `penv validate` or `penv fill` to see the names penv expects."
|
|
284
|
+
);
|
|
285
|
+
}
|
|
141
286
|
function keySourceFor(project, environment) {
|
|
142
287
|
return (0, import_core2.resolveKeySource)(project.config, environment);
|
|
143
288
|
}
|
|
@@ -493,36 +638,36 @@ async function runPush(options) {
|
|
|
493
638
|
decrypted: outbound.filter((item) => item.encrypted).length
|
|
494
639
|
};
|
|
495
640
|
}
|
|
496
|
-
function renderPush(
|
|
497
|
-
if (
|
|
641
|
+
function renderPush(result2) {
|
|
642
|
+
if (result2.pushed === 0) {
|
|
498
643
|
return formatRows([
|
|
499
644
|
{
|
|
500
645
|
glyph: CHECK,
|
|
501
646
|
label: "Nothing to push",
|
|
502
|
-
subject: `no values resolve for environment ${
|
|
647
|
+
subject: `no values resolve for environment ${result2.environment}`
|
|
503
648
|
}
|
|
504
649
|
]);
|
|
505
650
|
}
|
|
506
|
-
const target = `GitHub Actions for environment ${
|
|
651
|
+
const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
|
|
507
652
|
const rows = [
|
|
508
653
|
{
|
|
509
654
|
glyph: CHECK,
|
|
510
655
|
label: "Pushed",
|
|
511
|
-
subject: `${
|
|
656
|
+
subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
|
|
512
657
|
detail: `to ${target}`
|
|
513
658
|
},
|
|
514
659
|
{
|
|
515
660
|
glyph: CHECK,
|
|
516
661
|
label: "Scopes",
|
|
517
|
-
subject: `${
|
|
662
|
+
subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
|
|
518
663
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
519
664
|
}
|
|
520
665
|
];
|
|
521
|
-
if (
|
|
666
|
+
if (result2.decrypted > 0) {
|
|
522
667
|
rows.push({
|
|
523
668
|
glyph: WARN,
|
|
524
669
|
label: "Decrypted",
|
|
525
|
-
subject: `${
|
|
670
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
526
671
|
detail: "sent as plaintext for GitHub to re-seal under its own key"
|
|
527
672
|
});
|
|
528
673
|
}
|
|
@@ -542,19 +687,19 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
542
687
|
},
|
|
543
688
|
run({ args }) {
|
|
544
689
|
return guard(async () => {
|
|
545
|
-
const
|
|
690
|
+
const result2 = await runPush({
|
|
546
691
|
cwd: process.cwd(),
|
|
547
692
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
548
693
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
549
694
|
});
|
|
550
|
-
write(renderPush(
|
|
695
|
+
write(renderPush(result2));
|
|
551
696
|
});
|
|
552
697
|
}
|
|
553
698
|
});
|
|
554
699
|
|
|
555
700
|
// src/commands/validate.ts
|
|
556
|
-
var
|
|
557
|
-
var
|
|
701
|
+
var import_node_path3 = require("path");
|
|
702
|
+
var import_node_url2 = require("url");
|
|
558
703
|
var import_core6 = require("@penvhq/core");
|
|
559
704
|
var import_citty2 = require("citty");
|
|
560
705
|
var import_jiti = require("jiti");
|
|
@@ -623,16 +768,16 @@ function validationIssuesOf(cause, schemaPath) {
|
|
|
623
768
|
}
|
|
624
769
|
var schemaLoads = Promise.resolve();
|
|
625
770
|
function exclusively(work) {
|
|
626
|
-
const
|
|
627
|
-
schemaLoads =
|
|
771
|
+
const result2 = schemaLoads.then(work, work);
|
|
772
|
+
schemaLoads = result2.then(
|
|
628
773
|
() => void 0,
|
|
629
774
|
() => void 0
|
|
630
775
|
);
|
|
631
|
-
return
|
|
776
|
+
return result2;
|
|
632
777
|
}
|
|
633
778
|
function loadSchema(project, environment) {
|
|
634
779
|
const schemaPath = (0, import_core6.schemaFileOf)(project.config);
|
|
635
|
-
const file = (0,
|
|
780
|
+
const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
|
|
636
781
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
637
782
|
}
|
|
638
783
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
@@ -734,9 +879,9 @@ async function runValidate(options) {
|
|
|
734
879
|
}
|
|
735
880
|
}
|
|
736
881
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
737
|
-
const
|
|
738
|
-
if (!
|
|
739
|
-
for (const problem of
|
|
882
|
+
const result2 = schema.safeParse(object);
|
|
883
|
+
if (!result2.success) {
|
|
884
|
+
for (const problem of result2.error.issues) {
|
|
740
885
|
const path = problem.path.join(".");
|
|
741
886
|
if (undecryptable.has(path)) {
|
|
742
887
|
continue;
|
|
@@ -752,24 +897,24 @@ async function runValidate(options) {
|
|
|
752
897
|
}
|
|
753
898
|
return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
|
|
754
899
|
}
|
|
755
|
-
function renderValidate(
|
|
756
|
-
if (
|
|
900
|
+
function renderValidate(result2) {
|
|
901
|
+
if (result2.ok) {
|
|
757
902
|
return formatRows([
|
|
758
903
|
{
|
|
759
904
|
glyph: CHECK,
|
|
760
905
|
label: "Schema valid",
|
|
761
|
-
subject: `${
|
|
906
|
+
subject: `${result2.parameters} parameters for environment ${result2.environment}`
|
|
762
907
|
}
|
|
763
908
|
]);
|
|
764
909
|
}
|
|
765
|
-
const rows =
|
|
910
|
+
const rows = result2.issues.map((issue) => ({
|
|
766
911
|
glyph: WARN,
|
|
767
912
|
label: LABELS[issue.kind],
|
|
768
913
|
subject: issue.subject,
|
|
769
914
|
detail: issue.message
|
|
770
915
|
}));
|
|
771
916
|
const lines = formatRows(rows);
|
|
772
|
-
const remedies = [...new Set(
|
|
917
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
|
|
773
918
|
for (const remedy of remedies) {
|
|
774
919
|
if (remedy !== void 0) {
|
|
775
920
|
lines.push(` ${remedy}`);
|
|
@@ -787,12 +932,12 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
787
932
|
},
|
|
788
933
|
run({ args }) {
|
|
789
934
|
return guard(async () => {
|
|
790
|
-
const
|
|
935
|
+
const result2 = await runValidate({
|
|
791
936
|
cwd: process.cwd(),
|
|
792
937
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
793
938
|
});
|
|
794
|
-
write(renderValidate(
|
|
795
|
-
if (!
|
|
939
|
+
write(renderValidate(result2));
|
|
940
|
+
if (!result2.ok) {
|
|
796
941
|
process.exitCode = 1;
|
|
797
942
|
}
|
|
798
943
|
});
|
|
@@ -800,6 +945,7 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
800
945
|
});
|
|
801
946
|
|
|
802
947
|
// src/commands/doctor.ts
|
|
948
|
+
var STUCK_THRESHOLD_MS = 864e5;
|
|
803
949
|
function skipped(check, label) {
|
|
804
950
|
return {
|
|
805
951
|
check,
|
|
@@ -813,6 +959,8 @@ async function runDoctor(options) {
|
|
|
813
959
|
const project = openProject(options.cwd);
|
|
814
960
|
const environment = targetEnvironment(project, options.environment);
|
|
815
961
|
const findings = [];
|
|
962
|
+
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
963
|
+
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
816
964
|
const { schema, issues } = await loadSchema(project, environment);
|
|
817
965
|
if (schema !== void 0) {
|
|
818
966
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -861,6 +1009,14 @@ async function runDoctor(options) {
|
|
|
861
1009
|
findings.push(...plaintextSecretFindings(subjects, environment));
|
|
862
1010
|
findings.push(...publicSecretFindings(subjects, environment, project.config));
|
|
863
1011
|
findings.push(...encryptionFindings(subjects, environment));
|
|
1012
|
+
const rotation = await rotationSubjects(project, environment, subjects, options.source);
|
|
1013
|
+
if (rotation.kind === "unreachable") {
|
|
1014
|
+
findings.push(rotation.finding);
|
|
1015
|
+
} else {
|
|
1016
|
+
findings.push(...overdueFindings(rotation.subjects, environment, now));
|
|
1017
|
+
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1018
|
+
}
|
|
1019
|
+
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
864
1020
|
findings.push(...await sinkFindings(project, environment, options.sink));
|
|
865
1021
|
findings.push({
|
|
866
1022
|
check: "provider",
|
|
@@ -1278,6 +1434,246 @@ async function sinkFindings(project, environment, override) {
|
|
|
1278
1434
|
});
|
|
1279
1435
|
return findings;
|
|
1280
1436
|
}
|
|
1437
|
+
function humanizeMs(ms) {
|
|
1438
|
+
const abs = Math.max(0, ms);
|
|
1439
|
+
const day = 864e5;
|
|
1440
|
+
const hour = 36e5;
|
|
1441
|
+
const minute = 6e4;
|
|
1442
|
+
const round = (value, unit) => {
|
|
1443
|
+
const n = Math.round(value);
|
|
1444
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
1445
|
+
};
|
|
1446
|
+
if (abs >= day) return round(abs / day, "day");
|
|
1447
|
+
if (abs >= hour) return round(abs / hour, "hour");
|
|
1448
|
+
if (abs >= minute) return round(abs / minute, "minute");
|
|
1449
|
+
return round(abs / 1e3, "second");
|
|
1450
|
+
}
|
|
1451
|
+
function refPathOf(ref) {
|
|
1452
|
+
return [...ref.namespace, ref.name].join("/");
|
|
1453
|
+
}
|
|
1454
|
+
async function rotationSubjects(project, environment, local, override) {
|
|
1455
|
+
const providerConfig = project.config.providers[environment];
|
|
1456
|
+
if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
|
|
1457
|
+
return { kind: "read", subjects: local };
|
|
1458
|
+
}
|
|
1459
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1460
|
+
try {
|
|
1461
|
+
const subjects = await Promise.all(
|
|
1462
|
+
local.map(async ({ resolution }) => ({
|
|
1463
|
+
resolution,
|
|
1464
|
+
meta: await source.readMeta(resolution.ref)
|
|
1465
|
+
}))
|
|
1466
|
+
);
|
|
1467
|
+
return { kind: "read", subjects };
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
return {
|
|
1470
|
+
kind: "unreachable",
|
|
1471
|
+
finding: {
|
|
1472
|
+
check: "rotation-overdue",
|
|
1473
|
+
severity: "unknown",
|
|
1474
|
+
label: "Rotation",
|
|
1475
|
+
subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
|
|
1476
|
+
detail: errorDetail(error)
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function overdueFindings(subjects, environment, now) {
|
|
1482
|
+
const findings = [];
|
|
1483
|
+
for (const { resolution, meta } of subjects) {
|
|
1484
|
+
const { policy, lastRotated } = (0, import_core7.rotationOf)(meta, environment);
|
|
1485
|
+
if (policy === void 0 || lastRotated === null) {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const interval = (0, import_core7.tryParseDuration)(policy);
|
|
1489
|
+
if (interval === void 0) {
|
|
1490
|
+
findings.push({
|
|
1491
|
+
check: "rotation-overdue",
|
|
1492
|
+
severity: "warning",
|
|
1493
|
+
label: "Rotation policy invalid",
|
|
1494
|
+
subject: resolution.parameter,
|
|
1495
|
+
detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
|
|
1496
|
+
});
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const last = Date.parse(lastRotated);
|
|
1500
|
+
if (Number.isNaN(last)) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const overdueBy = now.getTime() - last - interval;
|
|
1504
|
+
if (overdueBy <= 0) {
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
findings.push({
|
|
1508
|
+
check: "rotation-overdue",
|
|
1509
|
+
severity: "warning",
|
|
1510
|
+
label: "Rotation overdue",
|
|
1511
|
+
subject: resolution.parameter,
|
|
1512
|
+
detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
|
|
1513
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
if (findings.length > 0) {
|
|
1517
|
+
return findings;
|
|
1518
|
+
}
|
|
1519
|
+
return [
|
|
1520
|
+
{
|
|
1521
|
+
check: "rotation-overdue",
|
|
1522
|
+
severity: "pass",
|
|
1523
|
+
label: "Rotation freshness",
|
|
1524
|
+
subject: `no parameter is past its rotation policy for ${environment}`
|
|
1525
|
+
}
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1528
|
+
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1529
|
+
const findings = [];
|
|
1530
|
+
for (const { resolution, meta } of subjects) {
|
|
1531
|
+
if (!(0, import_core7.isStuck)(meta, environment, now, stuckThresholdMs)) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const { rotatingSince } = (0, import_core7.rotationOf)(meta, environment);
|
|
1535
|
+
if (rotatingSince === null) {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const openFor = now.getTime() - Date.parse(rotatingSince);
|
|
1539
|
+
findings.push({
|
|
1540
|
+
check: "rotation-stuck",
|
|
1541
|
+
severity: "warning",
|
|
1542
|
+
label: "Rotation stuck",
|
|
1543
|
+
subject: resolution.parameter,
|
|
1544
|
+
detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
|
|
1545
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
if (findings.length > 0) {
|
|
1549
|
+
return findings;
|
|
1550
|
+
}
|
|
1551
|
+
return [
|
|
1552
|
+
{
|
|
1553
|
+
check: "rotation-stuck",
|
|
1554
|
+
severity: "pass",
|
|
1555
|
+
label: "Rotation progress",
|
|
1556
|
+
subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
|
|
1557
|
+
}
|
|
1558
|
+
];
|
|
1559
|
+
}
|
|
1560
|
+
function driftKey(file) {
|
|
1561
|
+
return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
|
|
1562
|
+
}
|
|
1563
|
+
function scopeKey(scope) {
|
|
1564
|
+
switch (scope.kind) {
|
|
1565
|
+
case "unscoped":
|
|
1566
|
+
return "unscoped";
|
|
1567
|
+
case "environment":
|
|
1568
|
+
return `environment:${scope.environment}`;
|
|
1569
|
+
case "local":
|
|
1570
|
+
return "local";
|
|
1571
|
+
case "environment-local":
|
|
1572
|
+
return `environment-local:${scope.environment}`;
|
|
1573
|
+
default:
|
|
1574
|
+
return (0, import_core7.assertNever)(scope, "scope");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function relevantToEnvironment(file, environment) {
|
|
1578
|
+
const scope = file.scope;
|
|
1579
|
+
if (scope.kind === "unscoped") return true;
|
|
1580
|
+
if (scope.kind === "environment") return scope.environment === environment;
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
async function readRelevant(provider, environment) {
|
|
1584
|
+
const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
|
|
1585
|
+
const stored = await Promise.all(files.map((file) => provider.read(file)));
|
|
1586
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const [index, file] of files.entries()) {
|
|
1588
|
+
const value = stored[index];
|
|
1589
|
+
if (value !== void 0) {
|
|
1590
|
+
entries.set(driftKey(file), { file, stored: value });
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return entries;
|
|
1594
|
+
}
|
|
1595
|
+
async function providerDriftFindings(project, environment, override) {
|
|
1596
|
+
const providerConfig = project.config.providers[environment];
|
|
1597
|
+
if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
|
|
1598
|
+
return [
|
|
1599
|
+
{
|
|
1600
|
+
check: "provider-value-drift",
|
|
1601
|
+
severity: "pass",
|
|
1602
|
+
label: "Provider values",
|
|
1603
|
+
subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
|
|
1604
|
+
}
|
|
1605
|
+
];
|
|
1606
|
+
}
|
|
1607
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1608
|
+
let local;
|
|
1609
|
+
let remote;
|
|
1610
|
+
try {
|
|
1611
|
+
local = await readRelevant(project.provider, environment);
|
|
1612
|
+
remote = await readRelevant(source, environment);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
return [
|
|
1615
|
+
{
|
|
1616
|
+
check: "provider-value-drift",
|
|
1617
|
+
severity: "unknown",
|
|
1618
|
+
label: "Provider values",
|
|
1619
|
+
subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
|
|
1620
|
+
detail: errorDetail(error)
|
|
1621
|
+
}
|
|
1622
|
+
];
|
|
1623
|
+
}
|
|
1624
|
+
const keys = keySourceFor(project, environment);
|
|
1625
|
+
const findings = [];
|
|
1626
|
+
const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
|
|
1627
|
+
for (const address of addresses) {
|
|
1628
|
+
const here = local.get(address);
|
|
1629
|
+
const there = remote.get(address);
|
|
1630
|
+
if (here !== void 0 && there !== void 0) {
|
|
1631
|
+
const opened = (0, import_core7.openValue)(here.file, here.stored, keys);
|
|
1632
|
+
if (opened.kind !== "plaintext") {
|
|
1633
|
+
findings.push({
|
|
1634
|
+
check: "provider-value-drift",
|
|
1635
|
+
severity: "unknown",
|
|
1636
|
+
label: "Provider value unreadable",
|
|
1637
|
+
subject: (0, import_core7.formatValueFile)(here.file),
|
|
1638
|
+
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1639
|
+
});
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (opened.value !== there.stored) {
|
|
1643
|
+
findings.push({
|
|
1644
|
+
check: "provider-value-drift",
|
|
1645
|
+
severity: "failure",
|
|
1646
|
+
label: "Provider value drift",
|
|
1647
|
+
subject: (0, import_core7.formatValueFile)(here.file),
|
|
1648
|
+
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const present = here ?? there;
|
|
1654
|
+
if (present === void 0) {
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
findings.push({
|
|
1658
|
+
check: "provider-value-drift",
|
|
1659
|
+
severity: "warning",
|
|
1660
|
+
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1661
|
+
subject: (0, import_core7.formatValueFile)(present.file),
|
|
1662
|
+
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`
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
if (findings.length > 0) {
|
|
1666
|
+
return findings;
|
|
1667
|
+
}
|
|
1668
|
+
return [
|
|
1669
|
+
{
|
|
1670
|
+
check: "provider-value-drift",
|
|
1671
|
+
severity: "pass",
|
|
1672
|
+
label: "Provider values",
|
|
1673
|
+
subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
|
|
1674
|
+
}
|
|
1675
|
+
];
|
|
1676
|
+
}
|
|
1281
1677
|
function renderDoctor(report) {
|
|
1282
1678
|
const rows = report.findings.map((finding) => ({
|
|
1283
1679
|
glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
|
|
@@ -1364,32 +1760,45 @@ function sealFor(project, file, value, parameter, environment) {
|
|
|
1364
1760
|
}
|
|
1365
1761
|
return (0, import_core8.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1366
1762
|
}
|
|
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);
|
|
1763
|
+
async function sealAwareWrite(options) {
|
|
1764
|
+
const { project, provider, ref, scope, value, environment } = options;
|
|
1765
|
+
const secret = (0, import_core8.isSecret)(await provider.readMeta(ref), environment);
|
|
1373
1766
|
const file = {
|
|
1374
1767
|
namespace: ref.namespace,
|
|
1375
1768
|
name: ref.name,
|
|
1376
1769
|
scope,
|
|
1377
1770
|
encrypted: secret
|
|
1378
1771
|
};
|
|
1379
|
-
const stored = secret ? sealFor(project, file,
|
|
1380
|
-
await
|
|
1381
|
-
await
|
|
1382
|
-
return {
|
|
1772
|
+
const stored = secret ? sealFor(project, file, value, (0, import_core8.parameterId)(ref), environment) : value;
|
|
1773
|
+
await provider.write(file, stored);
|
|
1774
|
+
await provider.remove({ ...file, encrypted: !secret });
|
|
1775
|
+
return { encrypted: secret, location: (0, import_core8.formatValueFile)(file) };
|
|
1776
|
+
}
|
|
1777
|
+
async function runSet(options) {
|
|
1778
|
+
const project = openProject(options.cwd);
|
|
1779
|
+
assertWritableKey(options.key);
|
|
1780
|
+
const ref = refFromKey(options.key, project.config);
|
|
1781
|
+
const scope = targetScope(project, options, options.key);
|
|
1782
|
+
const environment = policyEnvironment(project, options);
|
|
1783
|
+
const { encrypted, location } = await sealAwareWrite({
|
|
1784
|
+
project,
|
|
1785
|
+
provider: project.provider,
|
|
1786
|
+
ref,
|
|
1787
|
+
scope,
|
|
1788
|
+
value: options.value,
|
|
1789
|
+
environment
|
|
1790
|
+
});
|
|
1791
|
+
return { parameter: options.key, location, encrypted };
|
|
1383
1792
|
}
|
|
1384
|
-
function renderSet(
|
|
1793
|
+
function renderSet(result2) {
|
|
1385
1794
|
return formatRows([
|
|
1386
1795
|
{
|
|
1387
1796
|
glyph: CHECK,
|
|
1388
1797
|
label: "Wrote",
|
|
1389
|
-
subject: `${PENV_DIR}/${
|
|
1798
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1390
1799
|
// The `.enc` suffix says this already, but only to a reader who knows the
|
|
1391
1800
|
// grammar. Meta decided it, not the command line, so the command says so.
|
|
1392
|
-
...
|
|
1801
|
+
...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
1393
1802
|
}
|
|
1394
1803
|
]);
|
|
1395
1804
|
}
|
|
@@ -1525,13 +1934,13 @@ var UndecryptableAt = class extends import_core9.PenvError {
|
|
|
1525
1934
|
);
|
|
1526
1935
|
}
|
|
1527
1936
|
};
|
|
1528
|
-
function renderReseal(
|
|
1937
|
+
function renderReseal(result2, verb) {
|
|
1529
1938
|
return formatRows([
|
|
1530
1939
|
{
|
|
1531
1940
|
glyph: CHECK,
|
|
1532
1941
|
label: verb,
|
|
1533
|
-
subject: `${PENV_DIR}/${
|
|
1534
|
-
detail: `${PENV_DIR}/${
|
|
1942
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1943
|
+
detail: `${PENV_DIR}/${result2.removed} removed`
|
|
1535
1944
|
}
|
|
1536
1945
|
]);
|
|
1537
1946
|
}
|
|
@@ -1554,8 +1963,8 @@ var encryptCommand = (0, import_citty5.defineCommand)({
|
|
|
1554
1963
|
args: SCOPE_ARGS,
|
|
1555
1964
|
run({ args }) {
|
|
1556
1965
|
return guard(async () => {
|
|
1557
|
-
const
|
|
1558
|
-
write(renderReseal(
|
|
1966
|
+
const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1967
|
+
write(renderReseal(result2, "Encrypted"));
|
|
1559
1968
|
});
|
|
1560
1969
|
}
|
|
1561
1970
|
});
|
|
@@ -1564,23 +1973,128 @@ var decryptCommand = (0, import_citty5.defineCommand)({
|
|
|
1564
1973
|
args: SCOPE_ARGS,
|
|
1565
1974
|
run({ args }) {
|
|
1566
1975
|
return guard(async () => {
|
|
1567
|
-
const
|
|
1568
|
-
write(renderReseal(
|
|
1976
|
+
const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1977
|
+
write(renderReseal(result2, "Decrypted"));
|
|
1569
1978
|
});
|
|
1570
1979
|
}
|
|
1571
1980
|
});
|
|
1572
1981
|
|
|
1573
|
-
// src/commands/
|
|
1574
|
-
var
|
|
1575
|
-
var import_node_path3 = require("path");
|
|
1982
|
+
// src/commands/fill.ts
|
|
1983
|
+
var import_promises = require("readline/promises");
|
|
1576
1984
|
var import_core10 = require("@penvhq/core");
|
|
1577
1985
|
var import_citty6 = require("citty");
|
|
1986
|
+
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
1987
|
+
async function runFill(options) {
|
|
1988
|
+
const validation = await runValidate({
|
|
1989
|
+
cwd: options.cwd,
|
|
1990
|
+
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
1991
|
+
});
|
|
1992
|
+
const environment = validation.environment;
|
|
1993
|
+
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
1994
|
+
if (blockers.length > 0) {
|
|
1995
|
+
const detail = blockers.map(
|
|
1996
|
+
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
1997
|
+
).join("\n");
|
|
1998
|
+
throw new import_core10.PenvError(
|
|
1999
|
+
"FILL_BLOCKED",
|
|
2000
|
+
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
2001
|
+
${detail}`,
|
|
2002
|
+
`Fix these \u2014 \`penv validate\` reports them \u2014 then run \`penv fill\`. If you have not written a schema yet, declare the required parameters in ${PENV_DIR}/env.ts.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const written = [];
|
|
2006
|
+
const skipped2 = [];
|
|
2007
|
+
const unreachable = [];
|
|
2008
|
+
for (const drift of validation.drift.declared) {
|
|
2009
|
+
if (drift.ref === void 0) {
|
|
2010
|
+
unreachable.push({ subject: drift.subject, remedy: drift.remedy });
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
const ref = drift.ref;
|
|
2014
|
+
const key = [...ref.namespace, ref.name].join("/");
|
|
2015
|
+
const value = await options.ask({ parameter: key, environment, secret: false });
|
|
2016
|
+
if (value === void 0 || value === "") {
|
|
2017
|
+
skipped2.push(drift.subject);
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
const result2 = await runSet({ cwd: options.cwd, key, value, environment });
|
|
2021
|
+
written.push({
|
|
2022
|
+
parameter: drift.subject,
|
|
2023
|
+
location: result2.location,
|
|
2024
|
+
encrypted: result2.encrypted
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
return { environment, written, skipped: skipped2, unreachable };
|
|
2028
|
+
}
|
|
2029
|
+
function summaryLine(result2) {
|
|
2030
|
+
const filled = result2.written.length;
|
|
2031
|
+
if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
|
|
2032
|
+
return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
|
|
2033
|
+
}
|
|
2034
|
+
const parts = [`${filled} written`];
|
|
2035
|
+
if (result2.skipped.length > 0) {
|
|
2036
|
+
parts.push(`${result2.skipped.length} skipped`);
|
|
2037
|
+
}
|
|
2038
|
+
if (result2.unreachable.length > 0) {
|
|
2039
|
+
parts.push(`${result2.unreachable.length} unreachable`);
|
|
2040
|
+
}
|
|
2041
|
+
return `${parts.join(", ")} for environment ${result2.environment}`;
|
|
2042
|
+
}
|
|
2043
|
+
function renderFill(result2) {
|
|
2044
|
+
const rows = result2.written.map((entry) => ({
|
|
2045
|
+
glyph: CHECK,
|
|
2046
|
+
label: "Wrote",
|
|
2047
|
+
subject: `${PENV_DIR}/${entry.location}`,
|
|
2048
|
+
// Meta decided the seal, not the answer, so the line says so — as `set` does.
|
|
2049
|
+
...entry.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
2050
|
+
}));
|
|
2051
|
+
for (const entry of result2.unreachable) {
|
|
2052
|
+
rows.push({ glyph: WARN, label: "Unreachable", subject: entry.subject, detail: entry.remedy });
|
|
2053
|
+
}
|
|
2054
|
+
const lines = formatRows(rows);
|
|
2055
|
+
lines.push(summaryLine(result2));
|
|
2056
|
+
return lines;
|
|
2057
|
+
}
|
|
2058
|
+
var fillCommand = (0, import_citty6.defineCommand)({
|
|
2059
|
+
meta: {
|
|
2060
|
+
name: "fill",
|
|
2061
|
+
description: "Prompt for each declared parameter the tree has no value for"
|
|
2062
|
+
},
|
|
2063
|
+
args: {
|
|
2064
|
+
env: { type: "string", description: "The environment to fill" }
|
|
2065
|
+
},
|
|
2066
|
+
run({ args }) {
|
|
2067
|
+
return guard(async () => {
|
|
2068
|
+
const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2069
|
+
const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
|
|
2070
|
+
try {
|
|
2071
|
+
write(
|
|
2072
|
+
renderFill(
|
|
2073
|
+
await runFill({
|
|
2074
|
+
cwd: process.cwd(),
|
|
2075
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2076
|
+
ask
|
|
2077
|
+
})
|
|
2078
|
+
)
|
|
2079
|
+
);
|
|
2080
|
+
} finally {
|
|
2081
|
+
rl.close();
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
// src/commands/generate.ts
|
|
2088
|
+
var import_node_fs = require("fs");
|
|
2089
|
+
var import_node_path4 = require("path");
|
|
2090
|
+
var import_core11 = require("@penvhq/core");
|
|
2091
|
+
var import_citty7 = require("citty");
|
|
1578
2092
|
var DEFAULT_OUTPUT = ".env";
|
|
1579
2093
|
function entriesFor(project, environment, allowDecrypt) {
|
|
1580
2094
|
const keys = keySourceFor(project, environment);
|
|
1581
2095
|
const tree = localTree(project);
|
|
1582
2096
|
const resolutions = resolveAllSync(tree, environment, keys);
|
|
1583
|
-
const collision = (0,
|
|
2097
|
+
const collision = (0, import_core11.checkNameCollisions)(
|
|
1584
2098
|
refsFrom(resolutions.map((resolution) => resolution.ref)),
|
|
1585
2099
|
project.config
|
|
1586
2100
|
)[0];
|
|
@@ -1593,21 +2107,21 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
1593
2107
|
const winner = resolution.winner;
|
|
1594
2108
|
if (winner?.file.encrypted === true) {
|
|
1595
2109
|
if (!allowDecrypt) {
|
|
1596
|
-
throw new
|
|
2110
|
+
throw new import_core11.PenvError(
|
|
1597
2111
|
"ENCRYPTED_VALUE_REFUSED",
|
|
1598
2112
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
1599
2113
|
`Re-run with \`--allow-decrypt\` to write the decrypted value into the artifact, or generate for an environment whose values are plaintext. The artifact is gitignored; a committed plaintext secret is a \`penv doctor\` failure.`
|
|
1600
2114
|
);
|
|
1601
2115
|
}
|
|
1602
|
-
(0,
|
|
2116
|
+
(0, import_core11.requireValue)(resolution, environment);
|
|
1603
2117
|
decrypted += 1;
|
|
1604
2118
|
}
|
|
1605
2119
|
if (resolution.value === void 0) {
|
|
1606
2120
|
continue;
|
|
1607
2121
|
}
|
|
1608
|
-
const description = (0,
|
|
2122
|
+
const description = (0, import_core11.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
|
|
1609
2123
|
entries.push({
|
|
1610
|
-
key: (0,
|
|
2124
|
+
key: (0, import_core11.variableName)(resolution.ref, project.config),
|
|
1611
2125
|
value: resolution.value,
|
|
1612
2126
|
...typeof description === "string" ? { description } : {}
|
|
1613
2127
|
});
|
|
@@ -1618,40 +2132,40 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
1618
2132
|
function generateDotenv(options) {
|
|
1619
2133
|
const project = openProject(options.cwd);
|
|
1620
2134
|
const environment = targetEnvironment(project, options.environment);
|
|
1621
|
-
return (0,
|
|
2135
|
+
return (0, import_core11.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
|
|
1622
2136
|
}
|
|
1623
2137
|
function runGenerate(options) {
|
|
1624
2138
|
const project = openProject(options.cwd);
|
|
1625
2139
|
const environment = targetEnvironment(project, options.environment);
|
|
1626
2140
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
1627
|
-
const file = options.out === void 0 ? (0,
|
|
1628
|
-
(0, import_node_fs.writeFileSync)(file, (0,
|
|
2141
|
+
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);
|
|
2142
|
+
(0, import_node_fs.writeFileSync)(file, (0, import_core11.serializeDotenv)(entries), "utf8");
|
|
1629
2143
|
return { file, environment, entries: entries.length, decrypted };
|
|
1630
2144
|
}
|
|
1631
2145
|
function displayPath(cwd, file) {
|
|
1632
|
-
const rel = (0,
|
|
2146
|
+
const rel = (0, import_node_path4.relative)(cwd, file);
|
|
1633
2147
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
1634
2148
|
}
|
|
1635
|
-
function renderGenerate(
|
|
2149
|
+
function renderGenerate(result2, cwd) {
|
|
1636
2150
|
const rows = [
|
|
1637
2151
|
{
|
|
1638
2152
|
glyph: CHECK,
|
|
1639
2153
|
label: "Generated",
|
|
1640
|
-
subject: displayPath(cwd,
|
|
1641
|
-
detail: `${
|
|
2154
|
+
subject: displayPath(cwd, result2.file),
|
|
2155
|
+
detail: `${result2.entries} variables for environment ${result2.environment}`
|
|
1642
2156
|
}
|
|
1643
2157
|
];
|
|
1644
|
-
if (
|
|
2158
|
+
if (result2.decrypted > 0) {
|
|
1645
2159
|
rows.push({
|
|
1646
2160
|
glyph: WARN,
|
|
1647
2161
|
label: "Decrypted",
|
|
1648
|
-
subject: `${
|
|
2162
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
1649
2163
|
detail: "written as plaintext into the artifact"
|
|
1650
2164
|
});
|
|
1651
2165
|
}
|
|
1652
2166
|
return formatRows(rows);
|
|
1653
2167
|
}
|
|
1654
|
-
var generateCommand = (0,
|
|
2168
|
+
var generateCommand = (0, import_citty7.defineCommand)({
|
|
1655
2169
|
meta: { name: "generate", description: "Write a standard .env artifact for deploy targets" },
|
|
1656
2170
|
args: {
|
|
1657
2171
|
env: { type: "string", description: "The environment to generate for" },
|
|
@@ -1664,29 +2178,29 @@ var generateCommand = (0, import_citty6.defineCommand)({
|
|
|
1664
2178
|
run({ args }) {
|
|
1665
2179
|
return guard(async () => {
|
|
1666
2180
|
const cwd = process.cwd();
|
|
1667
|
-
const
|
|
2181
|
+
const result2 = runGenerate({
|
|
1668
2182
|
cwd,
|
|
1669
2183
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
1670
2184
|
...args.out === void 0 ? {} : { out: args.out },
|
|
1671
2185
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1672
2186
|
});
|
|
1673
|
-
write(renderGenerate(
|
|
2187
|
+
write(renderGenerate(result2, cwd));
|
|
1674
2188
|
});
|
|
1675
2189
|
}
|
|
1676
2190
|
});
|
|
1677
2191
|
|
|
1678
2192
|
// src/commands/get.ts
|
|
1679
|
-
var
|
|
1680
|
-
var
|
|
2193
|
+
var import_core12 = require("@penvhq/core");
|
|
2194
|
+
var import_citty8 = require("citty");
|
|
1681
2195
|
async function runGet(options) {
|
|
1682
2196
|
const project = openProject(options.cwd);
|
|
1683
2197
|
const environment = targetEnvironment(project, options.environment);
|
|
1684
2198
|
const ref = refFromKey(options.key);
|
|
1685
2199
|
const keys = keySourceFor(project, environment);
|
|
1686
|
-
const resolution = await (0,
|
|
1687
|
-
const value = (0,
|
|
2200
|
+
const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
|
|
2201
|
+
const value = (0, import_core12.requireValue)(resolution, environment);
|
|
1688
2202
|
if (value === void 0) {
|
|
1689
|
-
throw new
|
|
2203
|
+
throw new import_core12.PenvError(
|
|
1690
2204
|
"PARAMETER_ABSENT",
|
|
1691
2205
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
1692
2206
|
`Set it with \`penv set ${options.key} --env ${environment}\`, or run \`penv get ${options.key} --env ${environment} --explain\` to see every file penv looked at.`
|
|
@@ -1708,7 +2222,7 @@ async function runExplain(options) {
|
|
|
1708
2222
|
const environment = targetEnvironment(project, options.environment);
|
|
1709
2223
|
const ref = refFromKey(options.key);
|
|
1710
2224
|
const keys = keySourceFor(project, environment);
|
|
1711
|
-
const resolution = await (0,
|
|
2225
|
+
const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
|
|
1712
2226
|
const winner = resolution.winner;
|
|
1713
2227
|
return {
|
|
1714
2228
|
parameter: resolution.parameter,
|
|
@@ -1736,7 +2250,7 @@ function renderExplain(explanation) {
|
|
|
1736
2250
|
...columns(rows).map((line) => ` ${line}`)
|
|
1737
2251
|
];
|
|
1738
2252
|
}
|
|
1739
|
-
var getCommand = (0,
|
|
2253
|
+
var getCommand = (0, import_citty8.defineCommand)({
|
|
1740
2254
|
meta: { name: "get", description: "Read a parameter" },
|
|
1741
2255
|
args: {
|
|
1742
2256
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -1761,14 +2275,14 @@ var getCommand = (0, import_citty7.defineCommand)({
|
|
|
1761
2275
|
|
|
1762
2276
|
// src/commands/import.ts
|
|
1763
2277
|
var import_node_fs4 = require("fs");
|
|
1764
|
-
var
|
|
1765
|
-
var
|
|
1766
|
-
var
|
|
2278
|
+
var import_node_path7 = require("path");
|
|
2279
|
+
var import_core15 = require("@penvhq/core");
|
|
2280
|
+
var import_citty10 = require("citty");
|
|
1767
2281
|
|
|
1768
2282
|
// src/detect.ts
|
|
1769
2283
|
var import_node_fs2 = require("fs");
|
|
1770
|
-
var
|
|
1771
|
-
var
|
|
2284
|
+
var import_node_path5 = require("path");
|
|
2285
|
+
var import_core13 = require("@penvhq/core");
|
|
1772
2286
|
var SIGNATURES = [
|
|
1773
2287
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
1774
2288
|
{
|
|
@@ -1793,11 +2307,11 @@ function exportsSchema(file) {
|
|
|
1793
2307
|
);
|
|
1794
2308
|
}
|
|
1795
2309
|
function occupied(cwd, relative3) {
|
|
1796
|
-
const file = (0,
|
|
2310
|
+
const file = (0, import_node_path5.join)(cwd, ...relative3.split("/"));
|
|
1797
2311
|
return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
|
|
1798
2312
|
}
|
|
1799
2313
|
function schemaFileFor(cwd) {
|
|
1800
|
-
const dir = (0, import_node_fs2.existsSync)((0,
|
|
2314
|
+
const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
|
|
1801
2315
|
const preferred = `${dir}env.ts`;
|
|
1802
2316
|
if (!occupied(cwd, preferred)) {
|
|
1803
2317
|
return { file: preferred };
|
|
@@ -1806,7 +2320,7 @@ function schemaFileFor(cwd) {
|
|
|
1806
2320
|
if (!occupied(cwd, beside)) {
|
|
1807
2321
|
return { file: beside, displaced: preferred };
|
|
1808
2322
|
}
|
|
1809
|
-
return { file:
|
|
2323
|
+
return { file: import_core13.DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
1810
2324
|
}
|
|
1811
2325
|
function dependenciesOf(cwd) {
|
|
1812
2326
|
const manifest = manifestOf(cwd);
|
|
@@ -1825,7 +2339,7 @@ function dependenciesOf(cwd) {
|
|
|
1825
2339
|
return names;
|
|
1826
2340
|
}
|
|
1827
2341
|
function manifestOf(cwd) {
|
|
1828
|
-
const file = (0,
|
|
2342
|
+
const file = (0, import_node_path5.join)(cwd, "package.json");
|
|
1829
2343
|
if (!(0, import_node_fs2.existsSync)(file)) {
|
|
1830
2344
|
return void 0;
|
|
1831
2345
|
}
|
|
@@ -1868,10 +2382,10 @@ function hasImportsBlock(cwd) {
|
|
|
1868
2382
|
|
|
1869
2383
|
// src/commands/init.ts
|
|
1870
2384
|
var import_node_fs3 = require("fs");
|
|
1871
|
-
var
|
|
1872
|
-
var
|
|
1873
|
-
var
|
|
1874
|
-
var
|
|
2385
|
+
var import_node_path6 = require("path");
|
|
2386
|
+
var import_promises2 = require("readline/promises");
|
|
2387
|
+
var import_core14 = require("@penvhq/core");
|
|
2388
|
+
var import_citty9 = require("citty");
|
|
1875
2389
|
var CONFIG_FILE = "penv.config.ts";
|
|
1876
2390
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
1877
2391
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -1881,11 +2395,11 @@ var IMPORTS_PREFIX = "#";
|
|
|
1881
2395
|
var PACKAGE_FILE = "package.json";
|
|
1882
2396
|
var DEFAULT_DECISIONS = {
|
|
1883
2397
|
environments: [],
|
|
1884
|
-
schemaFile:
|
|
2398
|
+
schemaFile: import_core14.DEFAULT_SCHEMA_FILE,
|
|
1885
2399
|
publicPrefixes: [],
|
|
1886
2400
|
alias: DEFAULT_ALIAS
|
|
1887
2401
|
};
|
|
1888
|
-
var NOT_ENVIRONMENTS = [...
|
|
2402
|
+
var NOT_ENVIRONMENTS = [...import_core14.RESERVED_TOKENS, "example", "sample", "template"];
|
|
1889
2403
|
function suggestEnvironments(root) {
|
|
1890
2404
|
let entries;
|
|
1891
2405
|
try {
|
|
@@ -1901,7 +2415,7 @@ function suggestEnvironments(root) {
|
|
|
1901
2415
|
const segments = entry.slice(".env.".length).split(".");
|
|
1902
2416
|
const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
|
|
1903
2417
|
const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
|
|
1904
|
-
if (name === void 0 || !(0,
|
|
2418
|
+
if (name === void 0 || !(0, import_core14.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
|
|
1905
2419
|
continue;
|
|
1906
2420
|
}
|
|
1907
2421
|
found.add(name);
|
|
@@ -1909,10 +2423,10 @@ function suggestEnvironments(root) {
|
|
|
1909
2423
|
return [...found].sort();
|
|
1910
2424
|
}
|
|
1911
2425
|
function emptyFlag(flag) {
|
|
1912
|
-
return new
|
|
2426
|
+
return new import_core14.PenvError(
|
|
1913
2427
|
"INIT_FLAG_EMPTY",
|
|
1914
2428
|
`\`--${flag}\` was given without a value`,
|
|
1915
|
-
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${
|
|
2429
|
+
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core14.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
|
|
1916
2430
|
);
|
|
1917
2431
|
}
|
|
1918
2432
|
function splitEnvironments(value) {
|
|
@@ -1933,11 +2447,11 @@ function configOf(decisions) {
|
|
|
1933
2447
|
return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
|
|
1934
2448
|
}
|
|
1935
2449
|
function declaredIn(root) {
|
|
1936
|
-
const file = (0,
|
|
2450
|
+
const file = (0, import_node_path6.join)(root, CONFIG_FILE);
|
|
1937
2451
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
1938
2452
|
return void 0;
|
|
1939
2453
|
}
|
|
1940
|
-
return (0,
|
|
2454
|
+
return (0, import_core14.loadConfigFrom)(file);
|
|
1941
2455
|
}
|
|
1942
2456
|
function planInit(root, flags = {}) {
|
|
1943
2457
|
const declared = declaredIn(root);
|
|
@@ -1947,7 +2461,7 @@ function planInit(root, flags = {}) {
|
|
|
1947
2461
|
notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
|
|
1948
2462
|
} else if (detected === void 0) {
|
|
1949
2463
|
notes.push(
|
|
1950
|
-
`No framework detected in package.json \u2014 the schema goes to ${
|
|
2464
|
+
`No framework detected in package.json \u2014 the schema goes to ${import_core14.DEFAULT_SCHEMA_FILE}.`
|
|
1951
2465
|
);
|
|
1952
2466
|
} else {
|
|
1953
2467
|
notes.push(`Detected ${detected.name}.`);
|
|
@@ -1957,12 +2471,12 @@ function planInit(root, flags = {}) {
|
|
|
1957
2471
|
);
|
|
1958
2472
|
}
|
|
1959
2473
|
}
|
|
1960
|
-
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0,
|
|
2474
|
+
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core14.schemaFileOf)(declared) : detected?.schemaFile ?? import_core14.DEFAULT_SCHEMA_FILE : flags.schema.trim();
|
|
1961
2475
|
if (flags.schema !== void 0) {
|
|
1962
2476
|
if (schemaFile.length === 0) {
|
|
1963
2477
|
throw emptyFlag("schema");
|
|
1964
2478
|
}
|
|
1965
|
-
const error = (0,
|
|
2479
|
+
const error = (0, import_core14.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
|
|
1966
2480
|
if (error !== void 0) {
|
|
1967
2481
|
throw error;
|
|
1968
2482
|
}
|
|
@@ -1972,7 +2486,7 @@ function planInit(root, flags = {}) {
|
|
|
1972
2486
|
throw emptyFlag("alias");
|
|
1973
2487
|
}
|
|
1974
2488
|
if (!ALIAS_NAME.test(alias)) {
|
|
1975
|
-
throw new
|
|
2489
|
+
throw new import_core14.PenvError(
|
|
1976
2490
|
"INIT_ALIAS_INVALID",
|
|
1977
2491
|
`\`${alias}\` is not an alias penv can write`,
|
|
1978
2492
|
`An alias is \`@name\` \u2014 a tsconfig \`paths\` entry a bundler resolves \u2014 or \`#name\`, a package.json \`imports\` entry Node resolves itself. Those are the two things a module specifier can be that is not a package.`
|
|
@@ -2017,7 +2531,7 @@ function renderPlan(plan2) {
|
|
|
2017
2531
|
rows.push([
|
|
2018
2532
|
" schemaFile",
|
|
2019
2533
|
plan2.decisions.schemaFile,
|
|
2020
|
-
plan2.decisions.schemaFile ===
|
|
2534
|
+
plan2.decisions.schemaFile === import_core14.DEFAULT_SCHEMA_FILE ? "" : `(default: ${import_core14.DEFAULT_SCHEMA_FILE})`
|
|
2021
2535
|
]);
|
|
2022
2536
|
for (const prefix of plan2.decisions.publicPrefixes) {
|
|
2023
2537
|
rows.push([" publicPrefix", prefix, ""]);
|
|
@@ -2091,7 +2605,7 @@ ${providers} },
|
|
|
2091
2605
|
}
|
|
2092
2606
|
function renderConfigModule(decisions) {
|
|
2093
2607
|
let body = renderEnvironments(decisions);
|
|
2094
|
-
if (decisions.schemaFile !==
|
|
2608
|
+
if (decisions.schemaFile !== import_core14.DEFAULT_SCHEMA_FILE) {
|
|
2095
2609
|
body += `
|
|
2096
2610
|
// The module that exports the schema. It is yours \u2014 penv scaffolds it once
|
|
2097
2611
|
// and never regenerates it \u2014 so this says where you keep it.
|
|
@@ -2114,7 +2628,7 @@ ${body}});
|
|
|
2114
2628
|
`;
|
|
2115
2629
|
}
|
|
2116
2630
|
function renderGitignore(decisions) {
|
|
2117
|
-
const inside = (0,
|
|
2631
|
+
const inside = (0, import_core14.schemaInsideTree)(configOf(decisions));
|
|
2118
2632
|
const listed = inside === void 0 ? "" : `${inside}, `;
|
|
2119
2633
|
return `# Written by penv. Value files hold configuration values and are never
|
|
2120
2634
|
# committed; only the structure, ${listed}meta, and config are.
|
|
@@ -2259,13 +2773,13 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
2259
2773
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
2260
2774
|
}
|
|
2261
2775
|
function shapeError(what, target, alias) {
|
|
2262
|
-
return new
|
|
2776
|
+
return new import_core14.PenvError(
|
|
2263
2777
|
"TSCONFIG_SHAPE",
|
|
2264
2778
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
2265
2779
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
2266
2780
|
);
|
|
2267
2781
|
}
|
|
2268
|
-
function insertEnvAlias(source, target =
|
|
2782
|
+
function insertEnvAlias(source, target = import_core14.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
|
|
2269
2783
|
const alias = `"${name}": ["${target}"]`;
|
|
2270
2784
|
const root = skipTrivia(source, 0);
|
|
2271
2785
|
if (source.charAt(root) !== "{") {
|
|
@@ -2329,7 +2843,7 @@ function renderTsconfig(target, alias) {
|
|
|
2329
2843
|
`;
|
|
2330
2844
|
}
|
|
2331
2845
|
function ensurePenvDir(root) {
|
|
2332
|
-
const dir = (0,
|
|
2846
|
+
const dir = (0, import_node_path6.resolve)(root, PENV_DIR2);
|
|
2333
2847
|
if ((0, import_node_fs3.existsSync)(dir)) {
|
|
2334
2848
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
2335
2849
|
}
|
|
@@ -2337,7 +2851,7 @@ function ensurePenvDir(root) {
|
|
|
2337
2851
|
return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
|
|
2338
2852
|
}
|
|
2339
2853
|
function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
2340
|
-
const file = (0,
|
|
2854
|
+
const file = (0, import_node_path6.join)(root, ...decisions.schemaFile.split("/"));
|
|
2341
2855
|
if ((0, import_node_fs3.existsSync)(file)) {
|
|
2342
2856
|
return {
|
|
2343
2857
|
target: "schema",
|
|
@@ -2346,7 +2860,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2346
2860
|
note: "(yours \u2014 penv never regenerates it)"
|
|
2347
2861
|
};
|
|
2348
2862
|
}
|
|
2349
|
-
(0, import_node_fs3.mkdirSync)((0,
|
|
2863
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
2350
2864
|
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
|
|
2351
2865
|
return {
|
|
2352
2866
|
target: "schema",
|
|
@@ -2356,7 +2870,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2356
2870
|
};
|
|
2357
2871
|
}
|
|
2358
2872
|
function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
2359
|
-
const file = (0,
|
|
2873
|
+
const file = (0, import_node_path6.join)(root, CONFIG_FILE);
|
|
2360
2874
|
if ((0, import_node_fs3.existsSync)(file)) {
|
|
2361
2875
|
return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
|
|
2362
2876
|
}
|
|
@@ -2366,7 +2880,7 @@ function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
|
|
|
2366
2880
|
function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
2367
2881
|
const alias = decisions.alias;
|
|
2368
2882
|
const imports = alias.startsWith(IMPORTS_PREFIX);
|
|
2369
|
-
const file = (0,
|
|
2883
|
+
const file = (0, import_node_path6.join)(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
|
|
2370
2884
|
const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
|
|
2371
2885
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
2372
2886
|
if (imports) {
|
|
@@ -2409,14 +2923,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
|
|
|
2409
2923
|
};
|
|
2410
2924
|
}
|
|
2411
2925
|
function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
2412
|
-
const file = (0,
|
|
2926
|
+
const file = (0, import_node_path6.join)(root, PENV_DIR2, GITIGNORE_FILE);
|
|
2413
2927
|
const relative3 = `${PENV_DIR2}/${GITIGNORE_FILE}`;
|
|
2414
2928
|
const wanted = renderGitignore(decisions);
|
|
2415
2929
|
const existing = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : void 0;
|
|
2416
2930
|
if (existing === wanted) {
|
|
2417
2931
|
return { target: "gitignore", action: "kept", text: `Kept ${relative3}` };
|
|
2418
2932
|
}
|
|
2419
|
-
(0, import_node_fs3.mkdirSync)((0,
|
|
2933
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.join)(root, PENV_DIR2), { recursive: true });
|
|
2420
2934
|
(0, import_node_fs3.writeFileSync)(file, wanted, "utf8");
|
|
2421
2935
|
return {
|
|
2422
2936
|
target: "gitignore",
|
|
@@ -2434,26 +2948,26 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2434
2948
|
];
|
|
2435
2949
|
}
|
|
2436
2950
|
function runInit(options) {
|
|
2437
|
-
const root = (0,
|
|
2951
|
+
const root = (0, import_node_path6.resolve)(options.cwd);
|
|
2438
2952
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
2439
2953
|
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
2440
2954
|
}
|
|
2441
|
-
function renderInit(
|
|
2442
|
-
const steps =
|
|
2955
|
+
function renderInit(result2) {
|
|
2956
|
+
const steps = result2.steps.map((step) => {
|
|
2443
2957
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
2444
2958
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
2445
2959
|
});
|
|
2446
2960
|
return [
|
|
2447
2961
|
...formatSteps(steps),
|
|
2448
2962
|
"",
|
|
2449
|
-
`Done. Declare your parameters in ${
|
|
2450
|
-
...
|
|
2963
|
+
`Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
|
|
2964
|
+
...result2.decisions.environments.length === 0 ? [
|
|
2451
2965
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
2452
2966
|
] : []
|
|
2453
2967
|
];
|
|
2454
2968
|
}
|
|
2455
2969
|
async function askOnTty(plan2) {
|
|
2456
|
-
const rl = (0,
|
|
2970
|
+
const rl = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2457
2971
|
try {
|
|
2458
2972
|
return await promptForDecisions(plan2, {
|
|
2459
2973
|
ask: (question) => rl.question(question),
|
|
@@ -2464,7 +2978,7 @@ async function askOnTty(plan2) {
|
|
|
2464
2978
|
rl.close();
|
|
2465
2979
|
}
|
|
2466
2980
|
}
|
|
2467
|
-
var initCommand = (0,
|
|
2981
|
+
var initCommand = (0, import_citty9.defineCommand)({
|
|
2468
2982
|
meta: { name: "init", description: "Initialize a project (.penv/, env.ts, config, @env alias)" },
|
|
2469
2983
|
args: {
|
|
2470
2984
|
yes: {
|
|
@@ -2473,7 +2987,7 @@ var initCommand = (0, import_citty8.defineCommand)({
|
|
|
2473
2987
|
},
|
|
2474
2988
|
schema: {
|
|
2475
2989
|
type: "string",
|
|
2476
|
-
description: `Where the schema module goes, e.g. src/env.ts (default: ${
|
|
2990
|
+
description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core14.DEFAULT_SCHEMA_FILE})`
|
|
2477
2991
|
},
|
|
2478
2992
|
alias: {
|
|
2479
2993
|
type: "string",
|
|
@@ -2486,7 +3000,7 @@ var initCommand = (0, import_citty8.defineCommand)({
|
|
|
2486
3000
|
},
|
|
2487
3001
|
run({ args }) {
|
|
2488
3002
|
return guard(async () => {
|
|
2489
|
-
const root = (0,
|
|
3003
|
+
const root = (0, import_node_path6.resolve)(process.cwd());
|
|
2490
3004
|
const environments = environmentsFromFlag(args.env);
|
|
2491
3005
|
const plan2 = planInit(root, {
|
|
2492
3006
|
...args.schema === void 0 ? {} : { schema: args.schema },
|
|
@@ -2527,7 +3041,7 @@ function inferType(value) {
|
|
|
2527
3041
|
}
|
|
2528
3042
|
function draftFields(entries) {
|
|
2529
3043
|
return entries.map((entry) => {
|
|
2530
|
-
const key = (0,
|
|
3044
|
+
const key = (0, import_core15.accessPath)((0, import_core15.refFromVariable)(entry.key)).join(".");
|
|
2531
3045
|
return {
|
|
2532
3046
|
key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
|
|
2533
3047
|
type: inferType(entry.value)
|
|
@@ -2538,33 +3052,33 @@ function assertImportable(ref, variable) {
|
|
|
2538
3052
|
if (!ref.name.includes(".")) {
|
|
2539
3053
|
return;
|
|
2540
3054
|
}
|
|
2541
|
-
throw new
|
|
3055
|
+
throw new import_core15.PenvError(
|
|
2542
3056
|
"IMPORT_UNPARSEABLE_NAME",
|
|
2543
3057
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
2544
3058
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
2545
3059
|
);
|
|
2546
3060
|
}
|
|
2547
3061
|
function assertNotReserved(ref, variable, where, config) {
|
|
2548
|
-
if ((0,
|
|
2549
|
-
throw new
|
|
3062
|
+
if ((0, import_core15.isReservedToken)(ref.name, config)) {
|
|
3063
|
+
throw new import_core15.ReservedTokenError("parameter", variable, where);
|
|
2550
3064
|
}
|
|
2551
3065
|
}
|
|
2552
3066
|
function assertRoundTrips(ref, variable, config) {
|
|
2553
|
-
if ((0,
|
|
3067
|
+
if ((0, import_core15.roundTripsCleanly)(variable)) {
|
|
2554
3068
|
return;
|
|
2555
3069
|
}
|
|
2556
|
-
if ((0,
|
|
3070
|
+
if ((0, import_core15.variableName)(ref, config) === variable) {
|
|
2557
3071
|
return;
|
|
2558
3072
|
}
|
|
2559
|
-
const generated = (0,
|
|
2560
|
-
throw new
|
|
3073
|
+
const generated = (0, import_core15.variableName)(ref, config);
|
|
3074
|
+
throw new import_core15.PenvError(
|
|
2561
3075
|
"IMPORT_LOSSY_NAME",
|
|
2562
3076
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
2563
3077
|
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`names\` block of penv.config.ts \u2014 \`names: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
|
|
2564
3078
|
);
|
|
2565
3079
|
}
|
|
2566
3080
|
function collisionsIn(refs, config) {
|
|
2567
|
-
const errors = (0,
|
|
3081
|
+
const errors = (0, import_core15.checkNameCollisions)(refs, config);
|
|
2568
3082
|
const first = errors[0];
|
|
2569
3083
|
if (first !== void 0) {
|
|
2570
3084
|
throw first;
|
|
@@ -2574,10 +3088,10 @@ function assertDeclared(segment, config) {
|
|
|
2574
3088
|
if (config.environments.includes(segment)) {
|
|
2575
3089
|
return segment;
|
|
2576
3090
|
}
|
|
2577
|
-
throw new
|
|
3091
|
+
throw new import_core15.UnknownEnvironmentError(segment, config.environments);
|
|
2578
3092
|
}
|
|
2579
3093
|
function scopeFromFilename(file, config) {
|
|
2580
|
-
const name = (0,
|
|
3094
|
+
const name = (0, import_node_path7.basename)(file);
|
|
2581
3095
|
const segments = name.split(".");
|
|
2582
3096
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
2583
3097
|
if (start === -1) {
|
|
@@ -2599,13 +3113,13 @@ function scopeFromFilename(file, config) {
|
|
|
2599
3113
|
return { kind: "environment-local", environment: assertDeclared(first, config) };
|
|
2600
3114
|
}
|
|
2601
3115
|
if (first === LOCAL && rest.length === 2) {
|
|
2602
|
-
throw new
|
|
3116
|
+
throw new import_core15.FilenameGrammarError(
|
|
2603
3117
|
name,
|
|
2604
3118
|
"`local` precedes the environment segment",
|
|
2605
3119
|
`The environment segment always precedes \`local\` \u2014 \`.env.${second}.${LOCAL}\` is the file Next.js and Vite read, and \`.env.${LOCAL}.${second}\` is not a synonym for it. Rename it to \`.env.${second}.${LOCAL}\`, then import it again. Nothing was imported.`
|
|
2606
3120
|
);
|
|
2607
3121
|
}
|
|
2608
|
-
throw new
|
|
3122
|
+
throw new import_core15.FilenameGrammarError(
|
|
2609
3123
|
name,
|
|
2610
3124
|
`\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
|
|
2611
3125
|
"A dotenv file carries exactly one scope: `.env`, `.env.<environment>`, `.env.local`, or `.env.<environment>.local`. Point `penv import` at one of those. Nothing was imported."
|
|
@@ -2620,7 +3134,7 @@ function environmentOf(scope) {
|
|
|
2620
3134
|
case "local":
|
|
2621
3135
|
return void 0;
|
|
2622
3136
|
default:
|
|
2623
|
-
return (0,
|
|
3137
|
+
return (0, import_core15.assertNever)(scope, "scope");
|
|
2624
3138
|
}
|
|
2625
3139
|
}
|
|
2626
3140
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -2633,7 +3147,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
2633
3147
|
case "environment-local":
|
|
2634
3148
|
return scope;
|
|
2635
3149
|
default:
|
|
2636
|
-
return (0,
|
|
3150
|
+
return (0, import_core15.assertNever)(scope, "scope");
|
|
2637
3151
|
}
|
|
2638
3152
|
}
|
|
2639
3153
|
function explicitEnvironment(options, source, config) {
|
|
@@ -2641,7 +3155,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
2641
3155
|
if (value.length > 0) {
|
|
2642
3156
|
return value;
|
|
2643
3157
|
}
|
|
2644
|
-
throw new
|
|
3158
|
+
throw new import_core15.PenvError(
|
|
2645
3159
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
2646
3160
|
`\`--env\` for the import of ${source} names no environment`,
|
|
2647
3161
|
`Pass a declared environment \u2014 ${config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
|
|
@@ -2651,7 +3165,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
2651
3165
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
2652
3166
|
return;
|
|
2653
3167
|
}
|
|
2654
|
-
throw new
|
|
3168
|
+
throw new import_core15.PenvError(
|
|
2655
3169
|
"IMPORT_ENV_CONFLICT",
|
|
2656
3170
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
2657
3171
|
`Drop \`--env\` to import ${source} as ${derived}, pass \`--env ${derived}\` to say the same thing twice, or point \`penv import\` at the file that holds ${explicit}'s values. Nothing was imported.`
|
|
@@ -2661,7 +3175,7 @@ function environmentNamed(file, explicit) {
|
|
|
2661
3175
|
if (explicit !== void 0 && explicit.trim().length > 0) {
|
|
2662
3176
|
return explicit.trim();
|
|
2663
3177
|
}
|
|
2664
|
-
const segments = (0,
|
|
3178
|
+
const segments = (0, import_node_path7.basename)(file).split(".");
|
|
2665
3179
|
const start = segments.indexOf(DOTENV_SEGMENT);
|
|
2666
3180
|
if (start === -1) {
|
|
2667
3181
|
return void 0;
|
|
@@ -2672,15 +3186,15 @@ function environmentNamed(file, explicit) {
|
|
|
2672
3186
|
function decisionsOf(config, cwd) {
|
|
2673
3187
|
return {
|
|
2674
3188
|
environments: config.environments,
|
|
2675
|
-
schemaFile: (0,
|
|
3189
|
+
schemaFile: (0, import_core15.schemaFileOf)(config),
|
|
2676
3190
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
2677
3191
|
alias: detectAlias(cwd)
|
|
2678
3192
|
};
|
|
2679
3193
|
}
|
|
2680
3194
|
function configInEffect(cwd, environment) {
|
|
2681
|
-
const existing = (0,
|
|
3195
|
+
const existing = (0, import_core15.findConfigFile)(cwd);
|
|
2682
3196
|
if (existing !== void 0) {
|
|
2683
|
-
const config = (0,
|
|
3197
|
+
const config = (0, import_core15.loadConfigFrom)(existing);
|
|
2684
3198
|
return { config, decisions: decisionsOf(config, cwd) };
|
|
2685
3199
|
}
|
|
2686
3200
|
const planned = planInit(cwd).decisions;
|
|
@@ -2692,16 +3206,16 @@ function configInEffect(cwd, environment) {
|
|
|
2692
3206
|
return { config: openProject(cwd).config, decisions };
|
|
2693
3207
|
}
|
|
2694
3208
|
function importDotenv(options) {
|
|
2695
|
-
const cwd = (0,
|
|
2696
|
-
const file = (0,
|
|
3209
|
+
const cwd = (0, import_node_path7.resolve)(options.cwd);
|
|
3210
|
+
const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
|
|
2697
3211
|
if (!(0, import_node_fs4.existsSync)(file)) {
|
|
2698
|
-
throw new
|
|
3212
|
+
throw new import_core15.PenvError(
|
|
2699
3213
|
"IMPORT_FILE_MISSING",
|
|
2700
3214
|
`There is no file at ${file} to import`,
|
|
2701
3215
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
2702
3216
|
);
|
|
2703
3217
|
}
|
|
2704
|
-
const parsed = (0,
|
|
3218
|
+
const parsed = (0, import_core15.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
|
|
2705
3219
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
2706
3220
|
const source = displayPath2(cwd, file);
|
|
2707
3221
|
const named = scopeFromFilename(file, config);
|
|
@@ -2709,10 +3223,10 @@ function importDotenv(options) {
|
|
|
2709
3223
|
const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
|
|
2710
3224
|
assertEnvironmentAgrees(derived, explicit, source);
|
|
2711
3225
|
const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
|
|
2712
|
-
const environment = (0,
|
|
3226
|
+
const environment = (0, import_core15.lookupEnvironment)(config, explicit ?? derived);
|
|
2713
3227
|
const refs = [];
|
|
2714
3228
|
for (const entry of parsed.entries) {
|
|
2715
|
-
const ref = (0,
|
|
3229
|
+
const ref = (0, import_core15.refFromVariable)(entry.key);
|
|
2716
3230
|
assertImportable(ref, entry.key);
|
|
2717
3231
|
assertNotReserved(ref, entry.key, source, config);
|
|
2718
3232
|
assertRoundTrips(ref, entry.key, config);
|
|
@@ -2752,7 +3266,7 @@ function importDotenv(options) {
|
|
|
2752
3266
|
};
|
|
2753
3267
|
}
|
|
2754
3268
|
function displayPath2(root, file) {
|
|
2755
|
-
const rel = (0,
|
|
3269
|
+
const rel = (0, import_node_path7.relative)(root, file);
|
|
2756
3270
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
2757
3271
|
}
|
|
2758
3272
|
function keptSchemaStep(step, variables) {
|
|
@@ -2776,19 +3290,19 @@ function skippedValidationStep(environments) {
|
|
|
2776
3290
|
note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
|
|
2777
3291
|
};
|
|
2778
3292
|
}
|
|
2779
|
-
function renderImport(
|
|
2780
|
-
const steps = [{ glyph: CHECK, text: `Found ${
|
|
2781
|
-
if (
|
|
2782
|
-
const plural =
|
|
3293
|
+
function renderImport(result2, validation) {
|
|
3294
|
+
const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
|
|
3295
|
+
if (result2.orphanComments > 0) {
|
|
3296
|
+
const plural = result2.orphanComments === 1 ? "comment" : "comments";
|
|
2783
3297
|
steps.push({
|
|
2784
3298
|
glyph: WARN,
|
|
2785
|
-
text: `Dropped ${
|
|
3299
|
+
text: `Dropped ${result2.orphanComments} orphan ${plural}`,
|
|
2786
3300
|
note: "attached to no variable, so nothing to describe"
|
|
2787
3301
|
});
|
|
2788
3302
|
}
|
|
2789
|
-
for (const step of
|
|
3303
|
+
for (const step of result2.steps) {
|
|
2790
3304
|
if (step.target === "schema" && step.action === "kept") {
|
|
2791
|
-
steps.push(keptSchemaStep(step,
|
|
3305
|
+
steps.push(keptSchemaStep(step, result2.variables));
|
|
2792
3306
|
continue;
|
|
2793
3307
|
}
|
|
2794
3308
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
@@ -2796,10 +3310,10 @@ function renderImport(result, validation) {
|
|
|
2796
3310
|
step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
|
|
2797
3311
|
);
|
|
2798
3312
|
}
|
|
2799
|
-
steps.push({ glyph: CHECK, text: `Created ${displayPath2(
|
|
3313
|
+
steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
|
|
2800
3314
|
const lines = formatSteps(steps);
|
|
2801
3315
|
if (validation === void 0) {
|
|
2802
|
-
lines.push(...formatSteps([skippedValidationStep(
|
|
3316
|
+
lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
|
|
2803
3317
|
} else if (validation.ok) {
|
|
2804
3318
|
lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
|
|
2805
3319
|
} else {
|
|
@@ -2808,7 +3322,7 @@ function renderImport(result, validation) {
|
|
|
2808
3322
|
lines.push("", "Done. .penv/ is now your source of truth.");
|
|
2809
3323
|
return lines;
|
|
2810
3324
|
}
|
|
2811
|
-
var importCommand = (0,
|
|
3325
|
+
var importCommand = (0, import_citty10.defineCommand)({
|
|
2812
3326
|
meta: {
|
|
2813
3327
|
name: "import",
|
|
2814
3328
|
description: "Import an existing dotenv file; it becomes the source of truth"
|
|
@@ -2840,16 +3354,16 @@ var importCommand = (0, import_citty9.defineCommand)({
|
|
|
2840
3354
|
|
|
2841
3355
|
// src/commands/key.ts
|
|
2842
3356
|
var import_node_crypto = require("crypto");
|
|
2843
|
-
var
|
|
2844
|
-
var
|
|
3357
|
+
var import_core16 = require("@penvhq/core");
|
|
3358
|
+
var import_citty11 = require("citty");
|
|
2845
3359
|
|
|
2846
3360
|
// src/keychain.ts
|
|
2847
|
-
var
|
|
3361
|
+
var import_node_module2 = require("module");
|
|
2848
3362
|
var import_meta = {};
|
|
2849
3363
|
var cached;
|
|
2850
3364
|
function entryConstructor() {
|
|
2851
3365
|
if (cached === void 0) {
|
|
2852
|
-
const require2 = (0,
|
|
3366
|
+
const require2 = (0, import_node_module2.createRequire)(import_meta.url);
|
|
2853
3367
|
cached = require2("@napi-rs/keyring").Entry;
|
|
2854
3368
|
}
|
|
2855
3369
|
return cached;
|
|
@@ -2874,35 +3388,35 @@ function runKeyCreate(options) {
|
|
|
2874
3388
|
const environment = targetEnvironment(project, options.environment);
|
|
2875
3389
|
const declared = project.config.keys?.[environment];
|
|
2876
3390
|
if (declared === void 0) {
|
|
2877
|
-
throw new
|
|
3391
|
+
throw new import_core16.PenvError(
|
|
2878
3392
|
"KEY_SOURCE_UNDECLARED",
|
|
2879
3393
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
2880
3394
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
2881
3395
|
);
|
|
2882
3396
|
}
|
|
2883
|
-
const key = (0, import_node_crypto.randomBytes)(
|
|
3397
|
+
const key = (0, import_node_crypto.randomBytes)(import_core16.KEY_BYTES).toString("base64");
|
|
2884
3398
|
if (declared.source === "keychain") {
|
|
2885
3399
|
const keychain = options.keychain ?? defaultKeychain;
|
|
2886
3400
|
if (options.force !== true) {
|
|
2887
3401
|
let existing;
|
|
2888
3402
|
try {
|
|
2889
|
-
existing = keychain.getPassword(
|
|
3403
|
+
existing = keychain.getPassword(import_core16.KEYCHAIN_SERVICE, declared.id);
|
|
2890
3404
|
} catch (cause) {
|
|
2891
|
-
throw new
|
|
3405
|
+
throw new import_core16.PenvError(
|
|
2892
3406
|
"KEYCHAIN_UNAVAILABLE",
|
|
2893
3407
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
2894
3408
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2895
3409
|
);
|
|
2896
3410
|
}
|
|
2897
3411
|
if (existing !== null) {
|
|
2898
|
-
throw new
|
|
3412
|
+
throw new import_core16.PenvError(
|
|
2899
3413
|
"KEY_EXISTS",
|
|
2900
3414
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
2901
3415
|
"Replacing it would orphan every value already sealed under it \u2014 they could never be decrypted again. Re-run with `--force` only if you are certain nothing is sealed under the current key."
|
|
2902
3416
|
);
|
|
2903
3417
|
}
|
|
2904
3418
|
}
|
|
2905
|
-
keychain.setPassword(
|
|
3419
|
+
keychain.setPassword(import_core16.KEYCHAIN_SERVICE, declared.id, key);
|
|
2906
3420
|
return { source: "keychain", environment, id: declared.id };
|
|
2907
3421
|
}
|
|
2908
3422
|
return {
|
|
@@ -2913,29 +3427,29 @@ function runKeyCreate(options) {
|
|
|
2913
3427
|
key
|
|
2914
3428
|
};
|
|
2915
3429
|
}
|
|
2916
|
-
function renderKeyCreate(
|
|
2917
|
-
if (
|
|
3430
|
+
function renderKeyCreate(result2) {
|
|
3431
|
+
if (result2.source === "keychain") {
|
|
2918
3432
|
return [
|
|
2919
|
-
`A new key for environment ${
|
|
3433
|
+
`A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
2920
3434
|
"",
|
|
2921
3435
|
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
|
|
2922
3436
|
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
2923
3437
|
];
|
|
2924
3438
|
}
|
|
2925
3439
|
return [
|
|
2926
|
-
`A new key for environment ${
|
|
3440
|
+
`A new key for environment ${result2.environment}. penv did not store it.`,
|
|
2927
3441
|
"",
|
|
2928
|
-
` ${
|
|
3442
|
+
` ${result2.variable}=${result2.key}`,
|
|
2929
3443
|
"",
|
|
2930
3444
|
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
|
|
2931
3445
|
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
|
|
2932
3446
|
"without it, and penv keeps no copy to fall back on."
|
|
2933
3447
|
];
|
|
2934
3448
|
}
|
|
2935
|
-
var keyCommand = (0,
|
|
3449
|
+
var keyCommand = (0, import_citty11.defineCommand)({
|
|
2936
3450
|
meta: { name: "key", description: "Work with encryption keys" },
|
|
2937
3451
|
subCommands: {
|
|
2938
|
-
create: (0,
|
|
3452
|
+
create: (0, import_citty11.defineCommand)({
|
|
2939
3453
|
meta: { name: "create", description: "Generate a key for an environment" },
|
|
2940
3454
|
args: {
|
|
2941
3455
|
env: { type: "string", description: "The environment the key is for" },
|
|
@@ -2962,8 +3476,8 @@ var keyCommand = (0, import_citty10.defineCommand)({
|
|
|
2962
3476
|
});
|
|
2963
3477
|
|
|
2964
3478
|
// src/commands/list.ts
|
|
2965
|
-
var
|
|
2966
|
-
var
|
|
3479
|
+
var import_core17 = require("@penvhq/core");
|
|
3480
|
+
var import_citty12 = require("citty");
|
|
2967
3481
|
function scopeLabel2(scope) {
|
|
2968
3482
|
switch (scope.kind) {
|
|
2969
3483
|
case "environment":
|
|
@@ -2975,7 +3489,7 @@ function scopeLabel2(scope) {
|
|
|
2975
3489
|
case "unscoped":
|
|
2976
3490
|
return "default";
|
|
2977
3491
|
default:
|
|
2978
|
-
return (0,
|
|
3492
|
+
return (0, import_core17.assertNever)(scope, "scope");
|
|
2979
3493
|
}
|
|
2980
3494
|
}
|
|
2981
3495
|
async function runList(options) {
|
|
@@ -2983,12 +3497,12 @@ async function runList(options) {
|
|
|
2983
3497
|
const environment = targetEnvironment(project, options.environment);
|
|
2984
3498
|
const keys = keySourceFor(project, environment);
|
|
2985
3499
|
const parameters = [];
|
|
2986
|
-
for (const resolution of await (0,
|
|
3500
|
+
for (const resolution of await (0, import_core17.resolveAll)(environment, project.provider, keys)) {
|
|
2987
3501
|
const winner = resolution.winner;
|
|
2988
3502
|
const scope = winner?.file.scope;
|
|
2989
3503
|
parameters.push({
|
|
2990
3504
|
parameter: resolution.parameter,
|
|
2991
|
-
variable: (0,
|
|
3505
|
+
variable: (0, import_core17.variableName)(resolution.ref, project.config),
|
|
2992
3506
|
scope: scope === void 0 ? "absent" : scopeLabel2(scope),
|
|
2993
3507
|
location: winner?.location,
|
|
2994
3508
|
encrypted: winner?.file.encrypted === true,
|
|
@@ -2997,13 +3511,13 @@ async function runList(options) {
|
|
|
2997
3511
|
}
|
|
2998
3512
|
return { environment, parameters };
|
|
2999
3513
|
}
|
|
3000
|
-
function renderList(
|
|
3001
|
-
if (
|
|
3002
|
-
return [`No parameters in ${PENV_DIR}/ for environment ${
|
|
3514
|
+
function renderList(result2) {
|
|
3515
|
+
if (result2.parameters.length === 0) {
|
|
3516
|
+
return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
|
|
3003
3517
|
}
|
|
3004
|
-
return columns(
|
|
3518
|
+
return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
|
|
3005
3519
|
}
|
|
3006
|
-
var listCommand = (0,
|
|
3520
|
+
var listCommand = (0, import_citty12.defineCommand)({
|
|
3007
3521
|
meta: { name: "list", description: "List parameters" },
|
|
3008
3522
|
args: {
|
|
3009
3523
|
env: { type: "string", description: "The environment to resolve against" },
|
|
@@ -3011,18 +3525,18 @@ var listCommand = (0, import_citty11.defineCommand)({
|
|
|
3011
3525
|
},
|
|
3012
3526
|
run({ args }) {
|
|
3013
3527
|
return guard(async () => {
|
|
3014
|
-
const
|
|
3528
|
+
const result2 = await runList({
|
|
3015
3529
|
cwd: process.cwd(),
|
|
3016
3530
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
3017
3531
|
});
|
|
3018
|
-
write(args.json === true ? [JSON.stringify(
|
|
3532
|
+
write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
|
|
3019
3533
|
});
|
|
3020
3534
|
}
|
|
3021
3535
|
});
|
|
3022
3536
|
|
|
3023
3537
|
// src/commands/mv.ts
|
|
3024
|
-
var
|
|
3025
|
-
var
|
|
3538
|
+
var import_core18 = require("@penvhq/core");
|
|
3539
|
+
var import_citty13 = require("citty");
|
|
3026
3540
|
function environmentOf2(file) {
|
|
3027
3541
|
const scope = file.scope;
|
|
3028
3542
|
return scope.kind === "environment" || scope.kind === "environment-local" ? scope.environment : void 0;
|
|
@@ -3037,38 +3551,39 @@ async function planFile(project, source, target, parameter) {
|
|
|
3037
3551
|
}
|
|
3038
3552
|
const environment = environmentOf2(source);
|
|
3039
3553
|
if (environment === void 0) {
|
|
3040
|
-
throw new
|
|
3554
|
+
throw new import_core18.PenvError(
|
|
3041
3555
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3042
|
-
`${PENV_DIR}/${(0,
|
|
3556
|
+
`${PENV_DIR}/${(0, import_core18.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3043
3557
|
"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."
|
|
3044
3558
|
);
|
|
3045
3559
|
}
|
|
3046
3560
|
const keys = keySourceFor(project, environment);
|
|
3047
|
-
const opened = (0,
|
|
3561
|
+
const opened = (0, import_core18.openValue)(source, stored, keys);
|
|
3048
3562
|
if (opened.kind === "failed") {
|
|
3049
|
-
throw new
|
|
3563
|
+
throw new import_core18.PenvError(
|
|
3050
3564
|
"VALUE_UNDECRYPTABLE",
|
|
3051
|
-
`${PENV_DIR}/${(0,
|
|
3565
|
+
`${PENV_DIR}/${(0, import_core18.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3052
3566
|
"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."
|
|
3053
3567
|
);
|
|
3054
3568
|
}
|
|
3055
3569
|
return {
|
|
3056
3570
|
source,
|
|
3057
3571
|
target,
|
|
3058
|
-
contents: (0,
|
|
3572
|
+
contents: (0, import_core18.sealValue)(target, opened.value, keys, parameter, environment),
|
|
3059
3573
|
resealed: true
|
|
3060
3574
|
};
|
|
3061
3575
|
}
|
|
3062
3576
|
function filesOf(all, ref) {
|
|
3063
|
-
const id = (0,
|
|
3064
|
-
return all.filter((file) => (0,
|
|
3577
|
+
const id = (0, import_core18.parameterId)(ref);
|
|
3578
|
+
return all.filter((file) => (0, import_core18.parameterId)(file) === id);
|
|
3065
3579
|
}
|
|
3066
3580
|
async function runMove(options) {
|
|
3067
3581
|
const project = openProject(options.cwd);
|
|
3068
3582
|
const from = refFromKey(options.from, project.config);
|
|
3583
|
+
assertWritableKey(options.to);
|
|
3069
3584
|
const to = refFromKey(options.to, project.config);
|
|
3070
|
-
if ((0,
|
|
3071
|
-
throw new
|
|
3585
|
+
if ((0, import_core18.parameterId)(from) === (0, import_core18.parameterId)(to)) {
|
|
3586
|
+
throw new import_core18.PenvError(
|
|
3072
3587
|
"PARAMETER_UNCHANGED",
|
|
3073
3588
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3074
3589
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3078,24 +3593,24 @@ async function runMove(options) {
|
|
|
3078
3593
|
const sources = filesOf(all, from);
|
|
3079
3594
|
const meta = await project.provider.readMeta(from);
|
|
3080
3595
|
if (sources.length === 0 && meta === void 0) {
|
|
3081
|
-
throw new
|
|
3596
|
+
throw new import_core18.PenvError(
|
|
3082
3597
|
"PARAMETER_ABSENT",
|
|
3083
|
-
`Parameter ${(0,
|
|
3598
|
+
`Parameter ${(0, import_core18.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
|
|
3084
3599
|
`\`penv list\` shows every parameter penv holds.`
|
|
3085
3600
|
);
|
|
3086
3601
|
}
|
|
3087
3602
|
const occupied2 = filesOf(all, to);
|
|
3088
3603
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3089
|
-
throw new
|
|
3604
|
+
throw new import_core18.PenvError(
|
|
3090
3605
|
"PARAMETER_EXISTS",
|
|
3091
|
-
`Parameter ${(0,
|
|
3092
|
-
`Remove or rename ${(0,
|
|
3606
|
+
`Parameter ${(0, import_core18.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
|
|
3607
|
+
`Remove or rename ${(0, import_core18.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
3093
3608
|
);
|
|
3094
3609
|
}
|
|
3095
3610
|
const planned = [];
|
|
3096
3611
|
for (const source of sources) {
|
|
3097
3612
|
const target = { ...source, namespace: to.namespace, name: to.name };
|
|
3098
|
-
const one = await planFile(project, source, target, (0,
|
|
3613
|
+
const one = await planFile(project, source, target, (0, import_core18.parameterId)(to));
|
|
3099
3614
|
if (one !== void 0) {
|
|
3100
3615
|
planned.push(one);
|
|
3101
3616
|
}
|
|
@@ -3113,39 +3628,39 @@ async function runMove(options) {
|
|
|
3113
3628
|
await project.provider.removeMeta(from);
|
|
3114
3629
|
}
|
|
3115
3630
|
return {
|
|
3116
|
-
from: (0,
|
|
3117
|
-
to: (0,
|
|
3631
|
+
from: (0, import_core18.parameterId)(from),
|
|
3632
|
+
to: (0, import_core18.parameterId)(to),
|
|
3118
3633
|
files: planned.map((file) => ({
|
|
3119
|
-
from: (0,
|
|
3120
|
-
to: (0,
|
|
3634
|
+
from: (0, import_core18.formatValueFile)(file.source),
|
|
3635
|
+
to: (0, import_core18.formatValueFile)(file.target),
|
|
3121
3636
|
resealed: file.resealed
|
|
3122
3637
|
})),
|
|
3123
3638
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
3124
3639
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
3125
3640
|
// that printed the first while moving the second names no file on disk.
|
|
3126
|
-
meta: meta === void 0 ? void 0 : (0,
|
|
3127
|
-
schema: { was: (0,
|
|
3641
|
+
meta: meta === void 0 ? void 0 : (0, import_core18.formatMetaFile)({ ...to, format: "json" }),
|
|
3642
|
+
schema: { was: (0, import_core18.accessPath)(from).join("."), now: (0, import_core18.accessPath)(to).join(".") }
|
|
3128
3643
|
};
|
|
3129
3644
|
}
|
|
3130
|
-
function renderMove(
|
|
3131
|
-
const rows =
|
|
3645
|
+
function renderMove(result2) {
|
|
3646
|
+
const rows = result2.files.map((file) => ({
|
|
3132
3647
|
glyph: CHECK,
|
|
3133
3648
|
label: "Moved",
|
|
3134
3649
|
subject: `${PENV_DIR}/${file.to}`,
|
|
3135
3650
|
...file.resealed ? { detail: "re-sealed for its new address" } : {}
|
|
3136
3651
|
}));
|
|
3137
|
-
if (
|
|
3138
|
-
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${
|
|
3652
|
+
if (result2.meta !== void 0) {
|
|
3653
|
+
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
|
|
3139
3654
|
}
|
|
3140
3655
|
const lines = formatRows(rows);
|
|
3141
3656
|
lines.push(
|
|
3142
3657
|
"",
|
|
3143
|
-
` .penv/env.ts still declares \`${
|
|
3658
|
+
` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
|
|
3144
3659
|
" or `penv validate` will report the value as unused and the declaration as unset."
|
|
3145
3660
|
);
|
|
3146
3661
|
return lines;
|
|
3147
3662
|
}
|
|
3148
|
-
var mvCommand = (0,
|
|
3663
|
+
var mvCommand = (0, import_citty13.defineCommand)({
|
|
3149
3664
|
meta: { name: "mv", description: "Rename a parameter, every scope and its meta at once" },
|
|
3150
3665
|
args: {
|
|
3151
3666
|
from: {
|
|
@@ -3166,9 +3681,86 @@ var mvCommand = (0, import_citty12.defineCommand)({
|
|
|
3166
3681
|
}
|
|
3167
3682
|
});
|
|
3168
3683
|
|
|
3684
|
+
// src/commands/pull.ts
|
|
3685
|
+
var import_citty14 = require("citty");
|
|
3686
|
+
async function runPull(options) {
|
|
3687
|
+
const project = openProject(options.cwd);
|
|
3688
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3689
|
+
const source = await sourceProviderFor(project, environment);
|
|
3690
|
+
if (source.type === LOCAL_TREE_TYPE) {
|
|
3691
|
+
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3692
|
+
}
|
|
3693
|
+
const tree = localTree(project);
|
|
3694
|
+
const files = await source.list();
|
|
3695
|
+
let values = 0;
|
|
3696
|
+
for (const file of files) {
|
|
3697
|
+
const value = await source.read(file);
|
|
3698
|
+
if (value === void 0) {
|
|
3699
|
+
continue;
|
|
3700
|
+
}
|
|
3701
|
+
tree.writeSync(file, value);
|
|
3702
|
+
values += 1;
|
|
3703
|
+
}
|
|
3704
|
+
const refs = refsFrom(files);
|
|
3705
|
+
let meta = 0;
|
|
3706
|
+
for (const ref of refs) {
|
|
3707
|
+
const block = await source.readMeta(ref);
|
|
3708
|
+
if (block === void 0) {
|
|
3709
|
+
continue;
|
|
3710
|
+
}
|
|
3711
|
+
tree.writeMetaSync(ref, block);
|
|
3712
|
+
meta += 1;
|
|
3713
|
+
}
|
|
3714
|
+
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
3715
|
+
}
|
|
3716
|
+
function renderPull(result2) {
|
|
3717
|
+
if (result2.localSource) {
|
|
3718
|
+
return formatRows([
|
|
3719
|
+
{
|
|
3720
|
+
glyph: CHECK,
|
|
3721
|
+
label: "Nothing to pull",
|
|
3722
|
+
subject: `environment ${result2.environment} has no separate source of truth`,
|
|
3723
|
+
detail: "its values live in the local .penv tree already"
|
|
3724
|
+
}
|
|
3725
|
+
]);
|
|
3726
|
+
}
|
|
3727
|
+
return formatRows([
|
|
3728
|
+
{
|
|
3729
|
+
glyph: CHECK,
|
|
3730
|
+
label: "Pulled",
|
|
3731
|
+
subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
|
|
3732
|
+
detail: `from the ${result2.source} provider for environment ${result2.environment}`
|
|
3733
|
+
},
|
|
3734
|
+
{
|
|
3735
|
+
glyph: CHECK,
|
|
3736
|
+
label: "Parameters",
|
|
3737
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
|
|
3738
|
+
detail: "written into the local .penv tree"
|
|
3739
|
+
}
|
|
3740
|
+
]);
|
|
3741
|
+
}
|
|
3742
|
+
var pullCommand = (0, import_citty14.defineCommand)({
|
|
3743
|
+
meta: {
|
|
3744
|
+
name: "pull",
|
|
3745
|
+
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3746
|
+
},
|
|
3747
|
+
args: {
|
|
3748
|
+
env: { type: "string", description: "The environment to pull" }
|
|
3749
|
+
},
|
|
3750
|
+
run({ args }) {
|
|
3751
|
+
return guard(async () => {
|
|
3752
|
+
const result2 = await runPull({
|
|
3753
|
+
cwd: process.cwd(),
|
|
3754
|
+
...args.env === void 0 ? {} : { environment: args.env }
|
|
3755
|
+
});
|
|
3756
|
+
write(renderPull(result2));
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
|
|
3169
3761
|
// src/commands/remove.ts
|
|
3170
|
-
var
|
|
3171
|
-
var
|
|
3762
|
+
var import_core19 = require("@penvhq/core");
|
|
3763
|
+
var import_citty15 = require("citty");
|
|
3172
3764
|
async function runRemove(options) {
|
|
3173
3765
|
const project = openProject(options.cwd);
|
|
3174
3766
|
const ref = refFromKey(options.key);
|
|
@@ -3185,17 +3777,17 @@ async function runRemove(options) {
|
|
|
3185
3777
|
continue;
|
|
3186
3778
|
}
|
|
3187
3779
|
await project.provider.remove(file);
|
|
3188
|
-
removed.push((0,
|
|
3780
|
+
removed.push((0, import_core19.formatValueFile)(file));
|
|
3189
3781
|
}
|
|
3190
3782
|
return {
|
|
3191
3783
|
parameter: options.key,
|
|
3192
3784
|
removed,
|
|
3193
|
-
considered: files.map((file) => (0,
|
|
3785
|
+
considered: files.map((file) => (0, import_core19.formatValueFile)(file))
|
|
3194
3786
|
};
|
|
3195
3787
|
}
|
|
3196
|
-
function renderRemove(
|
|
3197
|
-
if (
|
|
3198
|
-
const first =
|
|
3788
|
+
function renderRemove(result2) {
|
|
3789
|
+
if (result2.removed.length === 0) {
|
|
3790
|
+
const first = result2.considered[0] ?? result2.parameter;
|
|
3199
3791
|
return formatRows([
|
|
3200
3792
|
{
|
|
3201
3793
|
glyph: WARN,
|
|
@@ -3205,14 +3797,14 @@ function renderRemove(result) {
|
|
|
3205
3797
|
}
|
|
3206
3798
|
]);
|
|
3207
3799
|
}
|
|
3208
|
-
const rows =
|
|
3800
|
+
const rows = result2.removed.map((location) => ({
|
|
3209
3801
|
glyph: CHECK,
|
|
3210
3802
|
label: "Removed",
|
|
3211
3803
|
subject: `${PENV_DIR}/${location}`
|
|
3212
3804
|
}));
|
|
3213
3805
|
return formatRows(rows);
|
|
3214
3806
|
}
|
|
3215
|
-
var removeCommand = (0,
|
|
3807
|
+
var removeCommand = (0, import_citty15.defineCommand)({
|
|
3216
3808
|
meta: { name: "remove", description: "Delete a parameter" },
|
|
3217
3809
|
args: {
|
|
3218
3810
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -3238,15 +3830,195 @@ var removeCommand = (0, import_citty13.defineCommand)({
|
|
|
3238
3830
|
}
|
|
3239
3831
|
});
|
|
3240
3832
|
|
|
3833
|
+
// src/commands/rotate.ts
|
|
3834
|
+
var import_core20 = require("@penvhq/core");
|
|
3835
|
+
var import_citty16 = require("citty");
|
|
3836
|
+
function rotatingFile(ref, environment) {
|
|
3837
|
+
return {
|
|
3838
|
+
namespace: ref.namespace,
|
|
3839
|
+
name: ref.name,
|
|
3840
|
+
scope: { kind: "environment", environment },
|
|
3841
|
+
encrypted: false
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
3845
|
+
if (provider.type === LOCAL_TREE_TYPE) {
|
|
3846
|
+
await sealAwareWrite({
|
|
3847
|
+
project,
|
|
3848
|
+
provider,
|
|
3849
|
+
ref,
|
|
3850
|
+
scope: { kind: "environment", environment },
|
|
3851
|
+
value,
|
|
3852
|
+
environment
|
|
3853
|
+
});
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
await provider.write(rotatingFile(ref, environment), value);
|
|
3857
|
+
}
|
|
3858
|
+
function requireNewValue(value, phase, key) {
|
|
3859
|
+
if (value === void 0) {
|
|
3860
|
+
throw new import_core20.PenvError(
|
|
3861
|
+
"ROTATION_NO_VALUE",
|
|
3862
|
+
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3863
|
+
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
return value;
|
|
3867
|
+
}
|
|
3868
|
+
function requireRetaining(provider, environment) {
|
|
3869
|
+
if (!(0, import_core20.retainsPrevious)(provider)) {
|
|
3870
|
+
throw new import_core20.PenvError(
|
|
3871
|
+
"ROTATION_NOT_RETAINING",
|
|
3872
|
+
`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`,
|
|
3873
|
+
"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."
|
|
3874
|
+
);
|
|
3875
|
+
}
|
|
3876
|
+
return provider;
|
|
3877
|
+
}
|
|
3878
|
+
async function runRotate(options) {
|
|
3879
|
+
const project = openProject(options.cwd);
|
|
3880
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3881
|
+
const ref = refFromKey(options.key, project.config);
|
|
3882
|
+
const provider = await sourceProviderFor(project, environment);
|
|
3883
|
+
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3884
|
+
const before = await provider.readMeta(ref);
|
|
3885
|
+
const { mechanism } = (0, import_core20.rotationOf)(before, environment);
|
|
3886
|
+
if (mechanism === void 0) {
|
|
3887
|
+
throw new import_core20.PenvError(
|
|
3888
|
+
"ROTATION_NO_MECHANISM",
|
|
3889
|
+
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3890
|
+
'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.'
|
|
3891
|
+
);
|
|
3892
|
+
}
|
|
3893
|
+
const begin = options.begin === true;
|
|
3894
|
+
const complete = options.complete === true;
|
|
3895
|
+
if (mechanism === "atomic-cutover") {
|
|
3896
|
+
if (begin || complete) {
|
|
3897
|
+
throw new import_core20.PenvError(
|
|
3898
|
+
"ROTATION_MECHANISM_MISMATCH",
|
|
3899
|
+
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3900
|
+
"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."
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3903
|
+
const value = requireNewValue(options.value, "cutover", options.key);
|
|
3904
|
+
await writeRotatedValue(project, provider, ref, environment, value);
|
|
3905
|
+
const after2 = (0, import_core20.completeRotation)(before, environment, nowIso);
|
|
3906
|
+
await provider.writeMeta(ref, after2);
|
|
3907
|
+
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
3908
|
+
}
|
|
3909
|
+
const retaining = requireRetaining(provider, environment);
|
|
3910
|
+
if (begin === complete) {
|
|
3911
|
+
throw new import_core20.PenvError(
|
|
3912
|
+
"ROTATION_PHASE_REQUIRED",
|
|
3913
|
+
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3914
|
+
"`--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."
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
if (begin) {
|
|
3918
|
+
const value = requireNewValue(options.value, "begin", options.key);
|
|
3919
|
+
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
3920
|
+
const after2 = (0, import_core20.beginRotation)(before, environment, nowIso);
|
|
3921
|
+
await retaining.writeMeta(ref, after2);
|
|
3922
|
+
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
3923
|
+
}
|
|
3924
|
+
const after = (0, import_core20.completeRotation)(before, environment, nowIso);
|
|
3925
|
+
await retaining.writeMeta(ref, after);
|
|
3926
|
+
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
3927
|
+
}
|
|
3928
|
+
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
3929
|
+
const { state, rotatingSince, lastRotated } = (0, import_core20.rotationOf)(after, environment);
|
|
3930
|
+
return {
|
|
3931
|
+
parameter: [...ref.namespace, ref.name].join("/"),
|
|
3932
|
+
environment,
|
|
3933
|
+
mechanism,
|
|
3934
|
+
phase,
|
|
3935
|
+
source,
|
|
3936
|
+
wroteValue,
|
|
3937
|
+
state,
|
|
3938
|
+
rotatingSince,
|
|
3939
|
+
lastRotated
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
function renderRotate(result2) {
|
|
3943
|
+
if (result2.phase === "begin") {
|
|
3944
|
+
return formatRows([
|
|
3945
|
+
{
|
|
3946
|
+
glyph: CHECK,
|
|
3947
|
+
label: "Rotating",
|
|
3948
|
+
subject: result2.parameter,
|
|
3949
|
+
detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
|
|
3950
|
+
},
|
|
3951
|
+
{
|
|
3952
|
+
glyph: CHECK,
|
|
3953
|
+
label: "Previous",
|
|
3954
|
+
subject: "still readable",
|
|
3955
|
+
detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
|
|
3956
|
+
}
|
|
3957
|
+
]);
|
|
3958
|
+
}
|
|
3959
|
+
if (result2.phase === "complete") {
|
|
3960
|
+
return formatRows([
|
|
3961
|
+
{
|
|
3962
|
+
glyph: CHECK,
|
|
3963
|
+
label: "Rotated",
|
|
3964
|
+
subject: result2.parameter,
|
|
3965
|
+
detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3966
|
+
}
|
|
3967
|
+
]);
|
|
3968
|
+
}
|
|
3969
|
+
return formatRows([
|
|
3970
|
+
{
|
|
3971
|
+
glyph: CHECK,
|
|
3972
|
+
label: "Rotated",
|
|
3973
|
+
subject: result2.parameter,
|
|
3974
|
+
detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
3975
|
+
}
|
|
3976
|
+
]);
|
|
3977
|
+
}
|
|
3978
|
+
var rotateCommand = (0, import_citty16.defineCommand)({
|
|
3979
|
+
meta: {
|
|
3980
|
+
name: "rotate",
|
|
3981
|
+
description: "Rotate a parameter's value by the mechanism its meta declares"
|
|
3982
|
+
},
|
|
3983
|
+
args: {
|
|
3984
|
+
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
3985
|
+
value: {
|
|
3986
|
+
type: "positional",
|
|
3987
|
+
required: false,
|
|
3988
|
+
description: "The new value; read from stdin if omitted. Not needed with --complete"
|
|
3989
|
+
},
|
|
3990
|
+
env: { type: "string", description: "The environment to rotate in" },
|
|
3991
|
+
begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
|
|
3992
|
+
complete: { type: "boolean", description: "Close a dual-valid grace window" }
|
|
3993
|
+
},
|
|
3994
|
+
run({ args }) {
|
|
3995
|
+
return guard(async () => {
|
|
3996
|
+
const value = args.complete === true ? void 0 : args.value ?? await readStdin();
|
|
3997
|
+
write(
|
|
3998
|
+
renderRotate(
|
|
3999
|
+
await runRotate({
|
|
4000
|
+
cwd: process.cwd(),
|
|
4001
|
+
key: args.key,
|
|
4002
|
+
...value === void 0 ? {} : { value },
|
|
4003
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4004
|
+
...args.begin === void 0 ? {} : { begin: args.begin },
|
|
4005
|
+
...args.complete === void 0 ? {} : { complete: args.complete }
|
|
4006
|
+
})
|
|
4007
|
+
)
|
|
4008
|
+
);
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
|
|
3241
4013
|
// src/commands/watch.ts
|
|
3242
4014
|
var import_node_fs5 = require("fs");
|
|
3243
|
-
var
|
|
3244
|
-
var
|
|
3245
|
-
var
|
|
4015
|
+
var import_node_path8 = require("path");
|
|
4016
|
+
var import_core21 = require("@penvhq/core");
|
|
4017
|
+
var import_citty17 = require("citty");
|
|
3246
4018
|
var DEBOUNCE_MS = 100;
|
|
3247
4019
|
function runWatch(options) {
|
|
3248
4020
|
const project = openProject(options.cwd);
|
|
3249
|
-
const configFile = (0,
|
|
4021
|
+
const configFile = (0, import_node_path8.basename)(project.configFile);
|
|
3250
4022
|
const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
|
|
3251
4023
|
const watchers = /* @__PURE__ */ new Set();
|
|
3252
4024
|
let timer;
|
|
@@ -3263,12 +4035,12 @@ function runWatch(options) {
|
|
|
3263
4035
|
}
|
|
3264
4036
|
running = true;
|
|
3265
4037
|
try {
|
|
3266
|
-
const
|
|
4038
|
+
const result2 = await runValidate({
|
|
3267
4039
|
cwd: options.cwd,
|
|
3268
4040
|
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
3269
4041
|
});
|
|
3270
4042
|
if (!closed) {
|
|
3271
|
-
options.onResult?.(
|
|
4043
|
+
options.onResult?.(result2);
|
|
3272
4044
|
}
|
|
3273
4045
|
} catch (error) {
|
|
3274
4046
|
if (!closed) {
|
|
@@ -3299,8 +4071,8 @@ function runWatch(options) {
|
|
|
3299
4071
|
watcher.close();
|
|
3300
4072
|
}
|
|
3301
4073
|
function armRecovery(target, recursive, only) {
|
|
3302
|
-
const parent = (0,
|
|
3303
|
-
const name = (0,
|
|
4074
|
+
const parent = (0, import_node_path8.dirname)(target);
|
|
4075
|
+
const name = (0, import_node_path8.basename)(target);
|
|
3304
4076
|
let recovery;
|
|
3305
4077
|
try {
|
|
3306
4078
|
recovery = (0, import_node_fs5.watch)(parent, { recursive: false }, (_event, filename) => {
|
|
@@ -3311,7 +4083,7 @@ function runWatch(options) {
|
|
|
3311
4083
|
stop(recovery);
|
|
3312
4084
|
return;
|
|
3313
4085
|
}
|
|
3314
|
-
if (filename !== null && (0,
|
|
4086
|
+
if (filename !== null && (0, import_node_path8.basename)(filename) !== name) {
|
|
3315
4087
|
return;
|
|
3316
4088
|
}
|
|
3317
4089
|
if (!(0, import_node_fs5.existsSync)(target)) {
|
|
@@ -3346,7 +4118,7 @@ function runWatch(options) {
|
|
|
3346
4118
|
schedule();
|
|
3347
4119
|
return;
|
|
3348
4120
|
}
|
|
3349
|
-
if (only !== void 0 && (filename === null || (0,
|
|
4121
|
+
if (only !== void 0 && (filename === null || (0, import_node_path8.basename)(filename) !== only)) {
|
|
3350
4122
|
return;
|
|
3351
4123
|
}
|
|
3352
4124
|
schedule();
|
|
@@ -3373,10 +4145,10 @@ function runWatch(options) {
|
|
|
3373
4145
|
watchers.add(watcher);
|
|
3374
4146
|
}
|
|
3375
4147
|
addWatcher(project.penvDir, true);
|
|
3376
|
-
addWatcher((0,
|
|
3377
|
-
if ((0,
|
|
3378
|
-
const schemaFile = (0,
|
|
3379
|
-
addWatcher((0,
|
|
4148
|
+
addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
|
|
4149
|
+
if ((0, import_core21.schemaInsideTree)(project.config) === void 0) {
|
|
4150
|
+
const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core21.schemaFileOf)(project.config));
|
|
4151
|
+
addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
|
|
3380
4152
|
}
|
|
3381
4153
|
void validate();
|
|
3382
4154
|
return {
|
|
@@ -3392,8 +4164,8 @@ function runWatch(options) {
|
|
|
3392
4164
|
}
|
|
3393
4165
|
};
|
|
3394
4166
|
}
|
|
3395
|
-
function renderWatch(
|
|
3396
|
-
return ["", ...renderValidate(
|
|
4167
|
+
function renderWatch(result2) {
|
|
4168
|
+
return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
|
|
3397
4169
|
}
|
|
3398
4170
|
function renderDrift(drift, environment) {
|
|
3399
4171
|
const rows = [
|
|
@@ -3419,7 +4191,7 @@ function renderDrift(drift, environment) {
|
|
|
3419
4191
|
}
|
|
3420
4192
|
return lines;
|
|
3421
4193
|
}
|
|
3422
|
-
var watchCommand = (0,
|
|
4194
|
+
var watchCommand = (0, import_citty17.defineCommand)({
|
|
3423
4195
|
meta: {
|
|
3424
4196
|
name: "watch",
|
|
3425
4197
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -3432,8 +4204,8 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3432
4204
|
const handle = runWatch({
|
|
3433
4205
|
cwd: process.cwd(),
|
|
3434
4206
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
3435
|
-
onResult: (
|
|
3436
|
-
write(renderWatch(
|
|
4207
|
+
onResult: (result2) => {
|
|
4208
|
+
write(renderWatch(result2));
|
|
3437
4209
|
},
|
|
3438
4210
|
// A failing cycle is reported and watching continues. `reportError`
|
|
3439
4211
|
// marks the process failed, which `validate` wants and a loop does not:
|
|
@@ -3447,10 +4219,10 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3447
4219
|
}
|
|
3448
4220
|
});
|
|
3449
4221
|
write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
|
|
3450
|
-
await new Promise((
|
|
4222
|
+
await new Promise((resolve7) => {
|
|
3451
4223
|
process.once("SIGINT", () => {
|
|
3452
4224
|
handle.close();
|
|
3453
|
-
|
|
4225
|
+
resolve7();
|
|
3454
4226
|
});
|
|
3455
4227
|
});
|
|
3456
4228
|
});
|
|
@@ -3458,7 +4230,7 @@ var watchCommand = (0, import_citty14.defineCommand)({
|
|
|
3458
4230
|
});
|
|
3459
4231
|
|
|
3460
4232
|
// src/index.ts
|
|
3461
|
-
var main = (0,
|
|
4233
|
+
var main = (0, import_citty18.defineCommand)({
|
|
3462
4234
|
meta: {
|
|
3463
4235
|
name: "penv",
|
|
3464
4236
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -3469,8 +4241,11 @@ var main = (0, import_citty15.defineCommand)({
|
|
|
3469
4241
|
generate: generateCommand,
|
|
3470
4242
|
get: getCommand,
|
|
3471
4243
|
set: setCommand,
|
|
4244
|
+
fill: fillCommand,
|
|
3472
4245
|
mv: mvCommand,
|
|
4246
|
+
pull: pullCommand,
|
|
3473
4247
|
push: pushCommand,
|
|
4248
|
+
rotate: rotateCommand,
|
|
3474
4249
|
remove: removeCommand,
|
|
3475
4250
|
list: listCommand,
|
|
3476
4251
|
encrypt: encryptCommand,
|
|
@@ -3482,8 +4257,8 @@ var main = (0, import_citty15.defineCommand)({
|
|
|
3482
4257
|
}
|
|
3483
4258
|
});
|
|
3484
4259
|
function runMain() {
|
|
3485
|
-
(0,
|
|
3486
|
-
return (0,
|
|
4260
|
+
(0, import_core22.setKeychain)(defaultKeychain);
|
|
4261
|
+
return (0, import_citty18.runMain)(main);
|
|
3487
4262
|
}
|
|
3488
4263
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3489
4264
|
0 && (module.exports = {
|
|
@@ -3493,21 +4268,27 @@ function runMain() {
|
|
|
3493
4268
|
insertEnvAlias,
|
|
3494
4269
|
main,
|
|
3495
4270
|
renderDoctor,
|
|
4271
|
+
renderFill,
|
|
3496
4272
|
renderMove,
|
|
4273
|
+
renderPull,
|
|
3497
4274
|
renderPush,
|
|
4275
|
+
renderRotate,
|
|
3498
4276
|
renderWatch,
|
|
3499
4277
|
runDecrypt,
|
|
3500
4278
|
runDoctor,
|
|
3501
4279
|
runEncrypt,
|
|
3502
4280
|
runExplain,
|
|
4281
|
+
runFill,
|
|
3503
4282
|
runGenerate,
|
|
3504
4283
|
runGet,
|
|
3505
4284
|
runInit,
|
|
3506
4285
|
runList,
|
|
3507
4286
|
runMain,
|
|
3508
4287
|
runMove,
|
|
4288
|
+
runPull,
|
|
3509
4289
|
runPush,
|
|
3510
4290
|
runRemove,
|
|
4291
|
+
runRotate,
|
|
3511
4292
|
runSet,
|
|
3512
4293
|
runValidate,
|
|
3513
4294
|
runWatch
|