@penvhq/cli 0.4.0 → 0.5.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 +686 -397
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -28
- package/dist/index.d.ts +49 -28
- package/dist/index.js +539 -241
- package/dist/index.js.map +1 -1
- package/package.json +8 -10
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
assertNever,
|
|
9
9
|
effectiveMeta,
|
|
10
10
|
formatValueFile as formatValueFile2,
|
|
11
|
+
holdsProjection as holdsProjection3,
|
|
12
|
+
holdsRecords,
|
|
11
13
|
isPublicVariable,
|
|
12
14
|
isRequired,
|
|
13
15
|
isSecret,
|
|
@@ -18,9 +20,66 @@ import {
|
|
|
18
20
|
tryParseDuration,
|
|
19
21
|
variableName as variableName3
|
|
20
22
|
} from "@penvhq/core";
|
|
21
|
-
import { createGithubSink as createGithubSink2 } from "@penvhq/sink-github";
|
|
22
23
|
import { defineCommand as defineCommand3 } from "citty";
|
|
23
24
|
|
|
25
|
+
// src/env-flags.ts
|
|
26
|
+
import { PenvError } from "@penvhq/core";
|
|
27
|
+
var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
|
|
28
|
+
var COMMAND_FLAGS = [
|
|
29
|
+
...BASE_RESERVED,
|
|
30
|
+
"env",
|
|
31
|
+
"out",
|
|
32
|
+
"allow-decrypt",
|
|
33
|
+
"destination",
|
|
34
|
+
"dest",
|
|
35
|
+
"d",
|
|
36
|
+
"location",
|
|
37
|
+
"l",
|
|
38
|
+
"yes",
|
|
39
|
+
"y"
|
|
40
|
+
];
|
|
41
|
+
function shadowedEnvironments(config) {
|
|
42
|
+
const flags = new Set(COMMAND_FLAGS);
|
|
43
|
+
return config.environments.filter((environment) => flags.has(environment));
|
|
44
|
+
}
|
|
45
|
+
function shorthandCandidates(args, declared) {
|
|
46
|
+
const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
|
|
47
|
+
return Object.keys(args).filter((key) => !reserved.has(key) && args[key] === true);
|
|
48
|
+
}
|
|
49
|
+
function quoteList(values) {
|
|
50
|
+
return values.map((value) => `\`--${value}\``).join(", ");
|
|
51
|
+
}
|
|
52
|
+
function environmentFromShorthand(config, candidates, explicit) {
|
|
53
|
+
if (candidates.length === 0) {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
const hits = candidates.filter((candidate) => config.environments.includes(candidate));
|
|
57
|
+
const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
|
|
58
|
+
if (strangers.length > 0) {
|
|
59
|
+
throw new PenvError(
|
|
60
|
+
"UNKNOWN_FLAG",
|
|
61
|
+
`${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
|
|
62
|
+
`Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (hits.length > 1) {
|
|
66
|
+
throw new PenvError(
|
|
67
|
+
"ENVIRONMENT_FLAG_AMBIGUOUS",
|
|
68
|
+
`${quoteList(hits)} name two environments at once, and a command acts on exactly one`,
|
|
69
|
+
"Pass a single environment flag, or the canonical `--env <name>`."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const hit = hits[0];
|
|
73
|
+
if (hit !== void 0 && explicit !== void 0 && explicit !== hit) {
|
|
74
|
+
throw new PenvError(
|
|
75
|
+
"ENVIRONMENT_FLAG_AMBIGUOUS",
|
|
76
|
+
`\`--env ${explicit}\` and \`--${hit}\` name two environments at once`,
|
|
77
|
+
"Drop one of them \u2014 `--env` is the canonical spelling."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return hit;
|
|
81
|
+
}
|
|
82
|
+
|
|
24
83
|
// src/project.ts
|
|
25
84
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
26
85
|
import {
|
|
@@ -30,7 +89,7 @@ import {
|
|
|
30
89
|
isReservedToken,
|
|
31
90
|
loadConfig,
|
|
32
91
|
openValue,
|
|
33
|
-
PenvError as
|
|
92
|
+
PenvError as PenvError3,
|
|
34
93
|
parameterId,
|
|
35
94
|
ReservedTokenError,
|
|
36
95
|
refFromAccessPath,
|
|
@@ -43,25 +102,19 @@ import { FilesystemProvider } from "@penvhq/provider-filesystem";
|
|
|
43
102
|
import { createRequire } from "module";
|
|
44
103
|
import { dirname, resolve } from "path";
|
|
45
104
|
import { pathToFileURL } from "url";
|
|
46
|
-
import { PenvError } from "@penvhq/core";
|
|
105
|
+
import { holdsProjection, PenvError as PenvError2 } from "@penvhq/core";
|
|
47
106
|
import { createFilesystemProvider } from "@penvhq/provider-filesystem";
|
|
48
|
-
import { createKubernetesProvider } from "@penvhq/provider-kubernetes";
|
|
49
107
|
import { createMockProvider } from "@penvhq/provider-mock";
|
|
50
|
-
import { createSsmProvider } from "@penvhq/provider-ssm";
|
|
51
|
-
import { createVaultProvider } from "@penvhq/provider-vault";
|
|
52
108
|
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
53
|
-
var LOCAL_TREE_TYPE = "filesystem";
|
|
109
|
+
var LOCAL_TREE_TYPE = "@penvhq/provider-filesystem";
|
|
54
110
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
55
111
|
[LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
|
|
56
|
-
["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
|
|
57
|
-
["ssm", ({ providerConfig }) => createSsmProvider({ path: providerConfig?.path ?? "penv" })],
|
|
58
112
|
[
|
|
59
|
-
"
|
|
60
|
-
({
|
|
61
|
-
]
|
|
62
|
-
["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
|
|
113
|
+
"@penvhq/provider-mock",
|
|
114
|
+
({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })
|
|
115
|
+
]
|
|
63
116
|
]);
|
|
64
|
-
var
|
|
117
|
+
var RECORD_CONTRACT_METHODS = [
|
|
65
118
|
"read",
|
|
66
119
|
"write",
|
|
67
120
|
"list",
|
|
@@ -70,6 +123,7 @@ var CONTRACT_METHODS = [
|
|
|
70
123
|
"writeMeta",
|
|
71
124
|
"removeMeta"
|
|
72
125
|
];
|
|
126
|
+
var PROJECTION_CONTRACT_METHODS = ["verify", "push", "list"];
|
|
73
127
|
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
74
128
|
function isProviderRegistered(type) {
|
|
75
129
|
return REGISTRY.has(type);
|
|
@@ -89,31 +143,30 @@ async function createSourceProvider(type, context) {
|
|
|
89
143
|
}
|
|
90
144
|
async function loadPluginProvider(type, context) {
|
|
91
145
|
const fromDir = resolutionBase(context);
|
|
92
|
-
const
|
|
93
|
-
const resolved = resolvePlugin(specifier, fromDir);
|
|
146
|
+
const resolved = resolvePlugin(type, fromDir);
|
|
94
147
|
if (resolved === void 0) {
|
|
95
|
-
throw unknownProvider(type, context.environment
|
|
148
|
+
throw unknownProvider(type, context.environment);
|
|
96
149
|
}
|
|
97
150
|
let mod;
|
|
98
151
|
try {
|
|
99
152
|
mod = await importPlugin(resolved);
|
|
100
153
|
} catch {
|
|
101
|
-
throw new
|
|
154
|
+
throw new PenvError2(
|
|
102
155
|
"PROVIDER_PLUGIN_LOAD",
|
|
103
|
-
`The provider package \`${
|
|
156
|
+
`The provider package \`${type}\` failed to load`,
|
|
104
157
|
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
105
158
|
);
|
|
106
159
|
}
|
|
107
160
|
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
108
161
|
if (typeof factory !== "function") {
|
|
109
|
-
throw new
|
|
162
|
+
throw new PenvError2(
|
|
110
163
|
"PROVIDER_PLUGIN_INVALID",
|
|
111
|
-
`\`${
|
|
164
|
+
`\`${type}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
112
165
|
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
113
166
|
);
|
|
114
167
|
}
|
|
115
168
|
const provider = await factory(context);
|
|
116
|
-
assertSatisfiesContract(provider,
|
|
169
|
+
assertSatisfiesContract(provider, type);
|
|
117
170
|
return provider;
|
|
118
171
|
}
|
|
119
172
|
function assertProvidersRegistered(config, projectRoot) {
|
|
@@ -121,15 +174,11 @@ function assertProvidersRegistered(config, projectRoot) {
|
|
|
121
174
|
if (isProviderRegistered(provider.type)) {
|
|
122
175
|
continue;
|
|
123
176
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
throw unknownProvider(provider.type, environment, specifier);
|
|
177
|
+
if (resolvePlugin(provider.type, projectRoot) === void 0) {
|
|
178
|
+
throw unknownProvider(provider.type, environment);
|
|
127
179
|
}
|
|
128
180
|
}
|
|
129
181
|
}
|
|
130
|
-
function pluginSpecifier(providerConfig, type) {
|
|
131
|
-
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
132
|
-
}
|
|
133
182
|
function resolutionBase(context) {
|
|
134
183
|
return dirname(context.root);
|
|
135
184
|
}
|
|
@@ -151,32 +200,26 @@ function importPlugin(resolvedPath) {
|
|
|
151
200
|
return loading;
|
|
152
201
|
}
|
|
153
202
|
function assertSatisfiesContract(provider, specifier) {
|
|
154
|
-
|
|
203
|
+
const projection = holdsProjection(provider);
|
|
204
|
+
const methods = projection ? PROJECTION_CONTRACT_METHODS : RECORD_CONTRACT_METHODS;
|
|
205
|
+
const contract = projection ? "the @penvhq/core ProjectionProvider contract its declared capabilities select" : "the @penvhq/core Provider contract that the filesystem provider defines";
|
|
206
|
+
for (const method of methods) {
|
|
155
207
|
if (typeof provider[method] !== "function") {
|
|
156
|
-
throw new
|
|
208
|
+
throw new PenvError2(
|
|
157
209
|
"PROVIDER_PLUGIN_INVALID",
|
|
158
210
|
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
159
|
-
|
|
211
|
+
`It must satisfy ${contract}.`
|
|
160
212
|
);
|
|
161
213
|
}
|
|
162
214
|
}
|
|
163
215
|
}
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
const slash = path.indexOf("/");
|
|
167
|
-
if (slash === -1) return { secretName: path };
|
|
168
|
-
const namespace = path.slice(0, slash);
|
|
169
|
-
const secretName = path.slice(slash + 1) || "penv";
|
|
170
|
-
return namespace === "" ? { secretName } : { namespace, secretName };
|
|
171
|
-
}
|
|
172
|
-
function unknownProvider(type, environment, specifier) {
|
|
173
|
-
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
216
|
+
function unknownProvider(type, environment) {
|
|
217
|
+
const preinstalled = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
174
218
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
175
|
-
|
|
176
|
-
return new PenvError(
|
|
219
|
+
return new PenvError2(
|
|
177
220
|
"UNKNOWN_PROVIDER",
|
|
178
|
-
`The provider
|
|
179
|
-
|
|
221
|
+
`The provider \`${type}\`${where} in penv.config.ts is not installed in this project`,
|
|
222
|
+
`Install it with \`npm i ${type}\` \u2014 a provider's \`type\` is the package penv imports. The CLI ships ${preinstalled} pre-installed.`
|
|
180
223
|
);
|
|
181
224
|
}
|
|
182
225
|
|
|
@@ -197,7 +240,7 @@ function openProject(cwd) {
|
|
|
197
240
|
}
|
|
198
241
|
function localTree(project) {
|
|
199
242
|
if (!(project.provider instanceof FilesystemProvider)) {
|
|
200
|
-
throw new
|
|
243
|
+
throw new PenvError3(
|
|
201
244
|
"PROVIDER_NOT_LOCAL",
|
|
202
245
|
`This command reads the local .penv tree synchronously, which the \`${project.provider.type}\` provider is not`,
|
|
203
246
|
"Run this against a filesystem-backed project, or use a command that speaks the async provider contract."
|
|
@@ -217,8 +260,9 @@ async function sourceProviderFor(project, environment) {
|
|
|
217
260
|
environment
|
|
218
261
|
});
|
|
219
262
|
}
|
|
220
|
-
function targetEnvironment(project, explicit) {
|
|
221
|
-
|
|
263
|
+
function targetEnvironment(project, explicit, shorthand) {
|
|
264
|
+
const fromFlags = shorthand === void 0 ? void 0 : environmentFromShorthand(project.config, shorthand, explicit);
|
|
265
|
+
return resolveEnvironment(project.config, explicit ?? fromFlags);
|
|
222
266
|
}
|
|
223
267
|
var KEY_SEPARATOR = /[./\\]/;
|
|
224
268
|
var NO_ENVIRONMENTS = { environments: [], providers: {} };
|
|
@@ -226,7 +270,7 @@ function refFromKey(key, config) {
|
|
|
226
270
|
const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
|
|
227
271
|
const name = segments[segments.length - 1];
|
|
228
272
|
if (name === void 0) {
|
|
229
|
-
throw new
|
|
273
|
+
throw new PenvError3(
|
|
230
274
|
"PARAMETER_KEY",
|
|
231
275
|
`\`${key}\` names no parameter`,
|
|
232
276
|
"A key is `<namespace>/<name>` or `<namespace>.<name>`, e.g. `redis/password`."
|
|
@@ -245,13 +289,13 @@ function assertWritableKey(key) {
|
|
|
245
289
|
const ref = refFromAccessPath(segments);
|
|
246
290
|
if (ref !== void 0) {
|
|
247
291
|
const suggestion = [...ref.namespace, ref.name].join("/");
|
|
248
|
-
throw new
|
|
292
|
+
throw new PenvError3(
|
|
249
293
|
"PARAMETER_KEY_CASING",
|
|
250
294
|
`\`${key}\` is not a canonical parameter name`,
|
|
251
295
|
`Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
|
|
252
296
|
);
|
|
253
297
|
}
|
|
254
|
-
throw new
|
|
298
|
+
throw new PenvError3(
|
|
255
299
|
"PARAMETER_KEY_UNREACHABLE",
|
|
256
300
|
`No value file can be named that reaches \`${key}\``,
|
|
257
301
|
"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."
|
|
@@ -540,7 +584,7 @@ function padVisible(text, width) {
|
|
|
540
584
|
}
|
|
541
585
|
|
|
542
586
|
// src/ui.ts
|
|
543
|
-
import { PenvError as
|
|
587
|
+
import { PenvError as PenvError4 } from "@penvhq/core";
|
|
544
588
|
var CHECK = "\u2713";
|
|
545
589
|
var WARN = "\u26A0";
|
|
546
590
|
var CROSS = "\u2717";
|
|
@@ -614,7 +658,7 @@ function write(lines) {
|
|
|
614
658
|
}
|
|
615
659
|
}
|
|
616
660
|
function reportError(error) {
|
|
617
|
-
if (error instanceof
|
|
661
|
+
if (error instanceof PenvError4) {
|
|
618
662
|
const suffix = error.remedy === void 0 ? void 0 : `
|
|
619
663
|
${error.remedy}`;
|
|
620
664
|
const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
|
|
@@ -648,31 +692,92 @@ async function guard(run) {
|
|
|
648
692
|
}
|
|
649
693
|
|
|
650
694
|
// src/commands/push.ts
|
|
651
|
-
import {
|
|
652
|
-
|
|
695
|
+
import {
|
|
696
|
+
checkNameCollisions,
|
|
697
|
+
holdsProjection as holdsProjection2,
|
|
698
|
+
PenvError as PenvError5,
|
|
699
|
+
requireValue,
|
|
700
|
+
variableName as variableName2
|
|
701
|
+
} from "@penvhq/core";
|
|
653
702
|
import { defineCommand } from "citty";
|
|
703
|
+
|
|
704
|
+
// src/input.ts
|
|
705
|
+
import { createInterface } from "readline";
|
|
706
|
+
function lineReader(input = process.stdin, output = process.stdout) {
|
|
707
|
+
const rl = createInterface({ input, output });
|
|
708
|
+
const buffered = [];
|
|
709
|
+
const waiting = [];
|
|
710
|
+
let closed = false;
|
|
711
|
+
rl.on("line", (line) => {
|
|
712
|
+
const next = waiting.shift();
|
|
713
|
+
if (next === void 0) {
|
|
714
|
+
buffered.push(line);
|
|
715
|
+
} else {
|
|
716
|
+
next(line);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
rl.on("close", () => {
|
|
720
|
+
closed = true;
|
|
721
|
+
for (const next of waiting.splice(0)) {
|
|
722
|
+
next(void 0);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
return {
|
|
726
|
+
ask(question) {
|
|
727
|
+
const early = buffered.shift();
|
|
728
|
+
if (early !== void 0) {
|
|
729
|
+
return Promise.resolve(early);
|
|
730
|
+
}
|
|
731
|
+
if (closed) {
|
|
732
|
+
return Promise.resolve(void 0);
|
|
733
|
+
}
|
|
734
|
+
rl.setPrompt(question);
|
|
735
|
+
rl.prompt();
|
|
736
|
+
return new Promise((resolve7) => {
|
|
737
|
+
waiting.push(resolve7);
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
close() {
|
|
741
|
+
rl.close();
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/commands/push.ts
|
|
654
747
|
var LAST_PUSHED_KEY = "lastPushedAt";
|
|
655
|
-
function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
748
|
+
async function destinationFor(project, environment, options) {
|
|
749
|
+
if (options.destination !== void 0) {
|
|
750
|
+
const providerConfig = {
|
|
751
|
+
type: options.destination,
|
|
752
|
+
...options.location === void 0 ? {} : { location: options.location }
|
|
753
|
+
};
|
|
754
|
+
const provider2 = options.provider ?? await createSourceProvider(options.destination, {
|
|
755
|
+
root: project.penvDir,
|
|
756
|
+
config: project.config,
|
|
757
|
+
providerConfig,
|
|
758
|
+
environment
|
|
759
|
+
});
|
|
760
|
+
return { provider: provider2, location: options.location };
|
|
761
|
+
}
|
|
762
|
+
const declared = project.config.providers[environment];
|
|
763
|
+
const location = declared?.location;
|
|
764
|
+
if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
|
|
765
|
+
throw new PenvError5(
|
|
766
|
+
"NO_DESTINATION",
|
|
767
|
+
`Environment ${environment}'s provider is the local .penv tree itself, so penv has nowhere to push`,
|
|
768
|
+
`Declare a provider for it in penv.config.ts \u2014 e.g. \`${environment}: { type: "@penvhq/provider-github", location: "owner/repo" }\` \u2014 or push somewhere once with \`penv push --env ${environment} --destination <package> --location <place>\`.`
|
|
662
769
|
);
|
|
663
770
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
return { sink: override, repo };
|
|
667
|
-
}
|
|
668
|
-
if (declared.type === "github") {
|
|
669
|
-
return { sink: createGithubSink(repo === void 0 ? {} : { repo }), repo };
|
|
771
|
+
if (options.provider !== void 0) {
|
|
772
|
+
return { provider: options.provider, location };
|
|
670
773
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
774
|
+
const provider = await createSourceProvider(declared.type, {
|
|
775
|
+
root: project.penvDir,
|
|
776
|
+
config: project.config,
|
|
777
|
+
providerConfig: declared,
|
|
778
|
+
environment
|
|
779
|
+
});
|
|
780
|
+
return { provider, location };
|
|
676
781
|
}
|
|
677
782
|
function plan(resolutions, config, environment, allowDecrypt) {
|
|
678
783
|
const outbound = [];
|
|
@@ -684,10 +789,10 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
684
789
|
let encrypted = false;
|
|
685
790
|
if (winner.file.encrypted) {
|
|
686
791
|
if (!allowDecrypt) {
|
|
687
|
-
throw new
|
|
792
|
+
throw new PenvError5(
|
|
688
793
|
"ENCRYPTED_VALUE_REFUSED",
|
|
689
|
-
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for
|
|
690
|
-
"Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the
|
|
794
|
+
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for the destination to re-seal`,
|
|
795
|
+
"Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the projection; the destination seals it under its own key."
|
|
691
796
|
);
|
|
692
797
|
}
|
|
693
798
|
requireValue(resolution, environment);
|
|
@@ -717,10 +822,46 @@ function recordPush(tree, ref, environment, iso) {
|
|
|
717
822
|
const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
|
|
718
823
|
tree.writeMetaSync(ref, meta);
|
|
719
824
|
}
|
|
720
|
-
async function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
825
|
+
async function terminalConfirm(question) {
|
|
826
|
+
if (process.stdin.isTTY !== true) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
const reader = lineReader();
|
|
830
|
+
try {
|
|
831
|
+
const answer = await reader.ask(`${question} [y/N] `);
|
|
832
|
+
return answer !== void 0 && /^y(es)?$/i.test(answer.trim());
|
|
833
|
+
} finally {
|
|
834
|
+
reader.close();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function ensureTargetApproved(provider, environment, options) {
|
|
838
|
+
if (provider.targetExists === void 0) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
if (await provider.targetExists(environment)) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (provider.ensureTarget === void 0) {
|
|
845
|
+
throw new PenvError5(
|
|
846
|
+
"MISSING_TARGET",
|
|
847
|
+
`The destination has no environment \`${environment}\` to receive this push, and this provider cannot create one`,
|
|
848
|
+
`Create the environment on the destination side, then run \`penv push --env ${environment}\` again.`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const approved = options.yes === true || await (options.confirm ?? terminalConfirm)(
|
|
852
|
+
`The destination has no environment \`${environment}\`. Create it?`
|
|
853
|
+
);
|
|
854
|
+
if (!approved) {
|
|
855
|
+
throw new PenvError5(
|
|
856
|
+
"MISSING_TARGET",
|
|
857
|
+
`The destination has no environment \`${environment}\` to receive this push`,
|
|
858
|
+
`Re-run with \`--yes\` to create it, answer \`y\` at the prompt, or create it on the destination side yourself.`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
await provider.ensureTarget(environment);
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
async function pushProjection(project, provider, environment, location, options) {
|
|
724
865
|
const tree = localTree(project);
|
|
725
866
|
const keys = keySourceFor(project, environment);
|
|
726
867
|
const resolutions = resolveAllSync(tree, environment, keys, true);
|
|
@@ -729,16 +870,17 @@ async function runPush(options) {
|
|
|
729
870
|
if (collision !== void 0) {
|
|
730
871
|
throw collision;
|
|
731
872
|
}
|
|
732
|
-
const nameError =
|
|
873
|
+
const nameError = provider.checkNames?.(refs, project.config)[0];
|
|
733
874
|
if (nameError !== void 0) {
|
|
734
875
|
throw nameError;
|
|
735
876
|
}
|
|
736
877
|
const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
|
|
737
|
-
await
|
|
878
|
+
await provider.verify();
|
|
879
|
+
const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
|
|
738
880
|
let repositorySecrets = 0;
|
|
739
881
|
let environmentSecrets = 0;
|
|
740
882
|
for (const item of outbound) {
|
|
741
|
-
await
|
|
883
|
+
await provider.push(item.variable, item.value, item.scope);
|
|
742
884
|
if (item.scope.kind === "repository") {
|
|
743
885
|
repositorySecrets += 1;
|
|
744
886
|
} else {
|
|
@@ -748,15 +890,70 @@ async function runPush(options) {
|
|
|
748
890
|
}
|
|
749
891
|
return {
|
|
750
892
|
environment,
|
|
751
|
-
|
|
893
|
+
destination: provider.type,
|
|
894
|
+
mode: "projection",
|
|
895
|
+
location,
|
|
752
896
|
pushed: outbound.length,
|
|
897
|
+
meta: 0,
|
|
753
898
|
repositorySecrets,
|
|
754
899
|
environmentSecrets,
|
|
755
|
-
decrypted: outbound.filter((item) => item.encrypted).length
|
|
900
|
+
decrypted: outbound.filter((item) => item.encrypted).length,
|
|
901
|
+
createdTarget
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function crossesToRecords(file, environment) {
|
|
905
|
+
const scope = file.scope;
|
|
906
|
+
if (scope.kind === "unscoped") {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
return scope.kind === "environment" && scope.environment === environment;
|
|
910
|
+
}
|
|
911
|
+
async function pushRecords(project, provider, environment, location) {
|
|
912
|
+
const tree = localTree(project);
|
|
913
|
+
const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
|
|
914
|
+
let pushed = 0;
|
|
915
|
+
for (const file of files) {
|
|
916
|
+
const value = tree.readSync(file);
|
|
917
|
+
if (value === void 0) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
await provider.write(file, value);
|
|
921
|
+
pushed += 1;
|
|
922
|
+
}
|
|
923
|
+
const refs = refsFrom(files);
|
|
924
|
+
let meta = 0;
|
|
925
|
+
for (const ref of refs) {
|
|
926
|
+
const block = tree.readMetaSync(ref);
|
|
927
|
+
if (block === void 0) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
await provider.writeMeta(ref, block);
|
|
931
|
+
meta += 1;
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
environment,
|
|
935
|
+
destination: provider.type,
|
|
936
|
+
mode: "records",
|
|
937
|
+
location,
|
|
938
|
+
pushed,
|
|
939
|
+
meta,
|
|
940
|
+
repositorySecrets: 0,
|
|
941
|
+
environmentSecrets: 0,
|
|
942
|
+
decrypted: 0,
|
|
943
|
+
createdTarget: false
|
|
756
944
|
};
|
|
757
945
|
}
|
|
946
|
+
async function runPush(options) {
|
|
947
|
+
const project = openProject(options.cwd);
|
|
948
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
949
|
+
const { provider, location } = await destinationFor(project, environment, options);
|
|
950
|
+
if (holdsProjection2(provider)) {
|
|
951
|
+
return pushProjection(project, provider, environment, location, options);
|
|
952
|
+
}
|
|
953
|
+
return pushRecords(project, provider, environment, location);
|
|
954
|
+
}
|
|
758
955
|
function renderPush(result2) {
|
|
759
|
-
if (result2.pushed === 0) {
|
|
956
|
+
if (result2.pushed === 0 && result2.meta === 0) {
|
|
760
957
|
return formatRows([
|
|
761
958
|
{
|
|
762
959
|
glyph: CHECK,
|
|
@@ -765,7 +962,23 @@ function renderPush(result2) {
|
|
|
765
962
|
}
|
|
766
963
|
]);
|
|
767
964
|
}
|
|
768
|
-
const target =
|
|
965
|
+
const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
|
|
966
|
+
if (result2.mode === "records") {
|
|
967
|
+
return formatRows([
|
|
968
|
+
{
|
|
969
|
+
glyph: CHECK,
|
|
970
|
+
label: "Pushed",
|
|
971
|
+
subject: `${result2.pushed} value ${result2.pushed === 1 ? "file" : "files"}, ${result2.meta} meta`,
|
|
972
|
+
detail: `mirrored verbatim to ${target}`
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
glyph: CHECK,
|
|
976
|
+
label: "Sealed values",
|
|
977
|
+
subject: "crossed byte-for-byte, still sealed",
|
|
978
|
+
detail: "the destination holds envelopes; no key was needed to push them"
|
|
979
|
+
}
|
|
980
|
+
]);
|
|
981
|
+
}
|
|
769
982
|
const rows = [
|
|
770
983
|
{
|
|
771
984
|
glyph: CHECK,
|
|
@@ -780,12 +993,20 @@ function renderPush(result2) {
|
|
|
780
993
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
781
994
|
}
|
|
782
995
|
];
|
|
996
|
+
if (result2.createdTarget) {
|
|
997
|
+
rows.push({
|
|
998
|
+
glyph: CHECK,
|
|
999
|
+
label: "Created",
|
|
1000
|
+
subject: `the destination environment ${result2.environment}`,
|
|
1001
|
+
detail: "it did not exist yet; created on your approval"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
783
1004
|
if (result2.decrypted > 0) {
|
|
784
1005
|
rows.push({
|
|
785
1006
|
glyph: WARN,
|
|
786
1007
|
label: "Decrypted",
|
|
787
1008
|
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
788
|
-
detail: "sent as plaintext for
|
|
1009
|
+
detail: "sent as plaintext for the destination to re-seal under its own key"
|
|
789
1010
|
});
|
|
790
1011
|
}
|
|
791
1012
|
return formatRows(rows);
|
|
@@ -793,13 +1014,28 @@ function renderPush(result2) {
|
|
|
793
1014
|
var pushCommand = defineCommand({
|
|
794
1015
|
meta: {
|
|
795
1016
|
name: "push",
|
|
796
|
-
description: "Push an environment's
|
|
1017
|
+
description: "Push an environment's values to its provider"
|
|
797
1018
|
},
|
|
798
1019
|
args: {
|
|
799
1020
|
env: { type: "string", description: "The environment to push" },
|
|
800
1021
|
"allow-decrypt": {
|
|
801
1022
|
type: "boolean",
|
|
802
|
-
description: "Decrypt sealed values locally and push them as plaintext for
|
|
1023
|
+
description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
|
|
1024
|
+
},
|
|
1025
|
+
destination: {
|
|
1026
|
+
type: "string",
|
|
1027
|
+
alias: ["dest", "d"],
|
|
1028
|
+
description: "Push once to this provider package instead of the declared one"
|
|
1029
|
+
},
|
|
1030
|
+
location: {
|
|
1031
|
+
type: "string",
|
|
1032
|
+
alias: "l",
|
|
1033
|
+
description: "The destination-side place, when --destination needs one"
|
|
1034
|
+
},
|
|
1035
|
+
yes: {
|
|
1036
|
+
type: "boolean",
|
|
1037
|
+
alias: "y",
|
|
1038
|
+
description: "Create a missing destination-side environment without prompting"
|
|
803
1039
|
}
|
|
804
1040
|
},
|
|
805
1041
|
run({ args }) {
|
|
@@ -807,7 +1043,22 @@ var pushCommand = defineCommand({
|
|
|
807
1043
|
const result2 = await runPush({
|
|
808
1044
|
cwd: process.cwd(),
|
|
809
1045
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
810
|
-
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1046
|
+
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
|
|
1047
|
+
...args.destination === void 0 ? {} : { destination: args.destination },
|
|
1048
|
+
...args.location === void 0 ? {} : { location: args.location },
|
|
1049
|
+
...args.yes === void 0 ? {} : { yes: args.yes },
|
|
1050
|
+
envFlags: shorthandCandidates(args, [
|
|
1051
|
+
"env",
|
|
1052
|
+
"allow-decrypt",
|
|
1053
|
+
"allowDecrypt",
|
|
1054
|
+
"destination",
|
|
1055
|
+
"dest",
|
|
1056
|
+
"d",
|
|
1057
|
+
"location",
|
|
1058
|
+
"l",
|
|
1059
|
+
"yes",
|
|
1060
|
+
"y"
|
|
1061
|
+
])
|
|
811
1062
|
});
|
|
812
1063
|
write(renderPush(result2));
|
|
813
1064
|
});
|
|
@@ -822,7 +1073,7 @@ import {
|
|
|
822
1073
|
checkNameCollisions as checkNameCollisions2,
|
|
823
1074
|
jitiFor,
|
|
824
1075
|
NameCollisionError,
|
|
825
|
-
PenvError as
|
|
1076
|
+
PenvError as PenvError6,
|
|
826
1077
|
ReservedTokenError as ReservedTokenError2,
|
|
827
1078
|
resolveAll,
|
|
828
1079
|
SCHEMA_HARVEST_ENV,
|
|
@@ -961,14 +1212,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
961
1212
|
}
|
|
962
1213
|
async function runValidate(options) {
|
|
963
1214
|
const project = openProject(options.cwd);
|
|
964
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1215
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
965
1216
|
const schemaPath = schemaFileOf(project.config);
|
|
966
1217
|
const issues = [];
|
|
967
1218
|
let files;
|
|
968
1219
|
try {
|
|
969
1220
|
files = await project.provider.list();
|
|
970
1221
|
} catch (error) {
|
|
971
|
-
if (!(error instanceof
|
|
1222
|
+
if (!(error instanceof PenvError6)) {
|
|
972
1223
|
throw error;
|
|
973
1224
|
}
|
|
974
1225
|
return {
|
|
@@ -1076,7 +1327,8 @@ var validateCommand = defineCommand2({
|
|
|
1076
1327
|
return guard(async () => {
|
|
1077
1328
|
const result2 = await runValidate({
|
|
1078
1329
|
cwd: process.cwd(),
|
|
1079
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
1330
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
1331
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1080
1332
|
});
|
|
1081
1333
|
write(renderValidate(result2));
|
|
1082
1334
|
if (!result2.ok) {
|
|
@@ -1099,10 +1351,19 @@ function skipped(check, label) {
|
|
|
1099
1351
|
}
|
|
1100
1352
|
async function runDoctor(options) {
|
|
1101
1353
|
const project = openProject(options.cwd);
|
|
1102
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1354
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
1103
1355
|
const findings = [];
|
|
1104
1356
|
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
1105
1357
|
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
1358
|
+
for (const shadowed of shadowedEnvironments(project.config)) {
|
|
1359
|
+
findings.push({
|
|
1360
|
+
check: "environment-flag-shadow",
|
|
1361
|
+
severity: "warning",
|
|
1362
|
+
label: "Environment shadows a flag",
|
|
1363
|
+
subject: shadowed,
|
|
1364
|
+
detail: `\`--${shadowed}\` is a flag penv defines, so this environment has no shorthand \u2014 \`--env ${shadowed}\` always works`
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1106
1367
|
const { schema, issues } = await loadSchema(project, environment);
|
|
1107
1368
|
if (schema !== void 0) {
|
|
1108
1369
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -1159,7 +1420,7 @@ async function runDoctor(options) {
|
|
|
1159
1420
|
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1160
1421
|
}
|
|
1161
1422
|
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
1162
|
-
findings.push(...await
|
|
1423
|
+
findings.push(...await projectionFindings(project, environment, options.projection));
|
|
1163
1424
|
findings.push({
|
|
1164
1425
|
check: "provider",
|
|
1165
1426
|
severity: "pass",
|
|
@@ -1395,15 +1656,6 @@ function encryptionFindings(subjects, environment) {
|
|
|
1395
1656
|
];
|
|
1396
1657
|
}
|
|
1397
1658
|
var EDIT_SKEW_MS = 12e4;
|
|
1398
|
-
function buildSink(declared, override) {
|
|
1399
|
-
if (override !== void 0) {
|
|
1400
|
-
return override;
|
|
1401
|
-
}
|
|
1402
|
-
if (declared.type === "github") {
|
|
1403
|
-
return createGithubSink2(declared.repo === void 0 ? {} : { repo: declared.repo });
|
|
1404
|
-
}
|
|
1405
|
-
return void 0;
|
|
1406
|
-
}
|
|
1407
1659
|
function errorDetail(error) {
|
|
1408
1660
|
if (error instanceof Error) {
|
|
1409
1661
|
return error.message.split("\n")[0] ?? error.message;
|
|
@@ -1413,32 +1665,43 @@ function errorDetail(error) {
|
|
|
1413
1665
|
function scopeLabel(scope) {
|
|
1414
1666
|
return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
|
|
1415
1667
|
}
|
|
1416
|
-
async function
|
|
1417
|
-
|
|
1418
|
-
if (
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1668
|
+
async function projectionFindings(project, environment, override) {
|
|
1669
|
+
let projection;
|
|
1670
|
+
if (override !== void 0) {
|
|
1671
|
+
projection = override;
|
|
1672
|
+
} else {
|
|
1673
|
+
const declared = project.config.providers[environment];
|
|
1674
|
+
if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
let source;
|
|
1678
|
+
try {
|
|
1679
|
+
source = await sourceProviderFor(project, environment);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
return [
|
|
1682
|
+
{
|
|
1683
|
+
check: "projection-unreachable",
|
|
1684
|
+
severity: "unknown",
|
|
1685
|
+
label: "Destination",
|
|
1686
|
+
subject: `could not build the ${declared.type} provider for ${environment}`,
|
|
1687
|
+
detail: errorDetail(error)
|
|
1688
|
+
}
|
|
1689
|
+
];
|
|
1690
|
+
}
|
|
1691
|
+
if (!holdsProjection3(source)) {
|
|
1692
|
+
return [];
|
|
1693
|
+
}
|
|
1694
|
+
projection = source;
|
|
1432
1695
|
}
|
|
1433
1696
|
try {
|
|
1434
|
-
await
|
|
1697
|
+
await projection.verify();
|
|
1435
1698
|
} catch (error) {
|
|
1436
1699
|
return [
|
|
1437
1700
|
{
|
|
1438
|
-
check: "
|
|
1701
|
+
check: "projection-unreachable",
|
|
1439
1702
|
severity: "unknown",
|
|
1440
|
-
label: "
|
|
1441
|
-
subject: `could not reach the ${
|
|
1703
|
+
label: "Destination",
|
|
1704
|
+
subject: `could not reach the ${projection.type} destination for ${environment}`,
|
|
1442
1705
|
detail: errorDetail(error)
|
|
1443
1706
|
}
|
|
1444
1707
|
];
|
|
@@ -1446,15 +1709,15 @@ async function sinkFindings(project, environment, override) {
|
|
|
1446
1709
|
let repoSecrets;
|
|
1447
1710
|
let envSecrets;
|
|
1448
1711
|
try {
|
|
1449
|
-
repoSecrets = await
|
|
1450
|
-
envSecrets = await
|
|
1712
|
+
repoSecrets = await projection.list({ kind: "repository" });
|
|
1713
|
+
envSecrets = await projection.list({ kind: "environment", environment });
|
|
1451
1714
|
} catch (error) {
|
|
1452
1715
|
return [
|
|
1453
1716
|
{
|
|
1454
|
-
check: "
|
|
1717
|
+
check: "projection-unreachable",
|
|
1455
1718
|
severity: "unknown",
|
|
1456
|
-
label: "
|
|
1457
|
-
subject: `could not list secrets in the ${
|
|
1719
|
+
label: "Destination",
|
|
1720
|
+
subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
|
|
1458
1721
|
detail: errorDetail(error)
|
|
1459
1722
|
}
|
|
1460
1723
|
];
|
|
@@ -1492,7 +1755,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1492
1755
|
}
|
|
1493
1756
|
if (!destOf(item.scope).has(key)) {
|
|
1494
1757
|
nameDrift.push({
|
|
1495
|
-
check: "
|
|
1758
|
+
check: "projection-name-drift",
|
|
1496
1759
|
severity: "warning",
|
|
1497
1760
|
label: "Declared, not pushed",
|
|
1498
1761
|
subject: item.variable,
|
|
@@ -1504,7 +1767,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1504
1767
|
for (const secret of repoSecrets) {
|
|
1505
1768
|
if (!allVariables.has(upper(secret.name))) {
|
|
1506
1769
|
nameDrift.push({
|
|
1507
|
-
check: "
|
|
1770
|
+
check: "projection-name-drift",
|
|
1508
1771
|
severity: "warning",
|
|
1509
1772
|
label: "In destination, not declared",
|
|
1510
1773
|
subject: secret.name,
|
|
@@ -1515,7 +1778,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1515
1778
|
for (const secret of envSecrets) {
|
|
1516
1779
|
if (!expectedEnv.has(upper(secret.name))) {
|
|
1517
1780
|
nameDrift.push({
|
|
1518
|
-
check: "
|
|
1781
|
+
check: "projection-name-drift",
|
|
1519
1782
|
severity: "warning",
|
|
1520
1783
|
label: "In destination, not declared",
|
|
1521
1784
|
subject: secret.name,
|
|
@@ -1539,7 +1802,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1539
1802
|
continue;
|
|
1540
1803
|
}
|
|
1541
1804
|
manualEdits.push({
|
|
1542
|
-
check: "
|
|
1805
|
+
check: "projection-manual-edit",
|
|
1543
1806
|
severity: "warning",
|
|
1544
1807
|
label: "Edited outside penv",
|
|
1545
1808
|
subject: item.variable,
|
|
@@ -1550,9 +1813,9 @@ async function sinkFindings(project, environment, override) {
|
|
|
1550
1813
|
findings.push(
|
|
1551
1814
|
...nameDrift.length > 0 ? nameDrift : [
|
|
1552
1815
|
{
|
|
1553
|
-
check: "
|
|
1816
|
+
check: "projection-name-drift",
|
|
1554
1817
|
severity: "pass",
|
|
1555
|
-
label: "
|
|
1818
|
+
label: "Destination names",
|
|
1556
1819
|
subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
|
|
1557
1820
|
}
|
|
1558
1821
|
]
|
|
@@ -1560,17 +1823,17 @@ async function sinkFindings(project, environment, override) {
|
|
|
1560
1823
|
findings.push(
|
|
1561
1824
|
...manualEdits.length > 0 ? manualEdits : [
|
|
1562
1825
|
{
|
|
1563
|
-
check: "
|
|
1826
|
+
check: "projection-manual-edit",
|
|
1564
1827
|
severity: "pass",
|
|
1565
|
-
label: "
|
|
1828
|
+
label: "Destination hand-edits",
|
|
1566
1829
|
subject: `no secret has changed outside penv since its last push for ${environment}`
|
|
1567
1830
|
}
|
|
1568
1831
|
]
|
|
1569
1832
|
);
|
|
1570
1833
|
findings.push({
|
|
1571
|
-
check: "
|
|
1834
|
+
check: "projection-value-drift",
|
|
1572
1835
|
severity: "unknown",
|
|
1573
|
-
label: "
|
|
1836
|
+
label: "Destination values",
|
|
1574
1837
|
subject: "cannot be read back from a write-only destination",
|
|
1575
1838
|
detail: "value drift between the tree and the destination is unknowable by design"
|
|
1576
1839
|
});
|
|
@@ -1599,6 +1862,9 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1599
1862
|
return { kind: "read", subjects: local };
|
|
1600
1863
|
}
|
|
1601
1864
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
1865
|
+
if (!holdsRecords(source)) {
|
|
1866
|
+
return { kind: "read", subjects: local };
|
|
1867
|
+
}
|
|
1602
1868
|
try {
|
|
1603
1869
|
const subjects = await Promise.all(
|
|
1604
1870
|
local.map(async ({ resolution }) => ({
|
|
@@ -1747,6 +2013,9 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1747
2013
|
];
|
|
1748
2014
|
}
|
|
1749
2015
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
2016
|
+
if (!holdsRecords(source)) {
|
|
2017
|
+
return [];
|
|
2018
|
+
}
|
|
1750
2019
|
let local;
|
|
1751
2020
|
let remote;
|
|
1752
2021
|
try {
|
|
@@ -1866,7 +2135,7 @@ function renderDoctor(report) {
|
|
|
1866
2135
|
var doctorCommand = defineCommand3({
|
|
1867
2136
|
meta: {
|
|
1868
2137
|
name: "doctor",
|
|
1869
|
-
description: "Report missing, weak, unused, fallback, plaintext-secret, and
|
|
2138
|
+
description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
|
|
1870
2139
|
},
|
|
1871
2140
|
args: {
|
|
1872
2141
|
env: { type: "string", description: "The environment to report on" }
|
|
@@ -1875,7 +2144,8 @@ var doctorCommand = defineCommand3({
|
|
|
1875
2144
|
return guard(async () => {
|
|
1876
2145
|
const report = await runDoctor({
|
|
1877
2146
|
cwd: process.cwd(),
|
|
1878
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
2147
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2148
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1879
2149
|
});
|
|
1880
2150
|
write(renderDoctor(report));
|
|
1881
2151
|
if (!report.ok) {
|
|
@@ -1890,14 +2160,14 @@ import {
|
|
|
1890
2160
|
formatValueFile as formatValueFile4,
|
|
1891
2161
|
isSecret as isSecret3,
|
|
1892
2162
|
openValue as openValue3,
|
|
1893
|
-
PenvError as
|
|
2163
|
+
PenvError as PenvError8,
|
|
1894
2164
|
parameterId as parameterId4,
|
|
1895
2165
|
sealValue as sealValue2
|
|
1896
2166
|
} from "@penvhq/core";
|
|
1897
2167
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1898
2168
|
|
|
1899
2169
|
// src/commands/set.ts
|
|
1900
|
-
import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as
|
|
2170
|
+
import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError7, parameterId as parameterId3, sealValue } from "@penvhq/core";
|
|
1901
2171
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1902
2172
|
function scopeFrom(options) {
|
|
1903
2173
|
if (options.local === true) {
|
|
@@ -1917,7 +2187,7 @@ function targetScope(project, options, key) {
|
|
|
1917
2187
|
return scopeFrom(options);
|
|
1918
2188
|
}
|
|
1919
2189
|
if (environment.trim().length === 0) {
|
|
1920
|
-
throw new
|
|
2190
|
+
throw new PenvError7(
|
|
1921
2191
|
"ENVIRONMENT_FLAG_EMPTY",
|
|
1922
2192
|
`\`--env\` for parameter ${key} names no environment`,
|
|
1923
2193
|
`Pass a declared environment \u2014 ${project.config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
|
|
@@ -1930,7 +2200,7 @@ function policyEnvironment(project, options) {
|
|
|
1930
2200
|
}
|
|
1931
2201
|
function sealFor(project, file, value, parameter, environment) {
|
|
1932
2202
|
if (environment === void 0) {
|
|
1933
|
-
throw new
|
|
2203
|
+
throw new PenvError7(
|
|
1934
2204
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1935
2205
|
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
|
|
1936
2206
|
"Keys are declared per environment in the `keys` block of penv.config.ts, so penv cannot tell which key should seal a file that every environment reads. Write it at an environment scope \u2014 add `--env <environment>` \u2014 or drop `secret` from the parameter's meta."
|
|
@@ -2032,7 +2302,7 @@ function twins(project, key, options) {
|
|
|
2032
2302
|
}
|
|
2033
2303
|
function environmentFor(project, options, verb) {
|
|
2034
2304
|
if (options.environment === void 0) {
|
|
2035
|
-
throw new
|
|
2305
|
+
throw new PenvError8(
|
|
2036
2306
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
2037
2307
|
`\`penv ${verb}\` names no environment, and keys are declared per environment`,
|
|
2038
2308
|
"Pass `--env <environment>`. penv cannot tell which environment's key applies to a file that names none, and will not pick one for you."
|
|
@@ -2051,7 +2321,7 @@ async function runEncrypt(options) {
|
|
|
2051
2321
|
const value = await readOne(project, plain);
|
|
2052
2322
|
if (value === void 0) {
|
|
2053
2323
|
const already = await readOne(project, sealed);
|
|
2054
|
-
throw new
|
|
2324
|
+
throw new PenvError8(
|
|
2055
2325
|
"PARAMETER_ABSENT",
|
|
2056
2326
|
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile4(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
2057
2327
|
already === void 0 ? `Write it first with \`penv set ${options.key} --env ${environment}\`, which seals it automatically when the parameter's meta declares it a secret.` : "Nothing to do."
|
|
@@ -2072,7 +2342,7 @@ async function runDecrypt(options) {
|
|
|
2072
2342
|
const [plain, sealed] = twins(project, options.key, options);
|
|
2073
2343
|
const parameter = parameterId4(plain);
|
|
2074
2344
|
if (isSecret3(await project.provider.readMeta(plain), environment)) {
|
|
2075
|
-
throw new
|
|
2345
|
+
throw new PenvError8(
|
|
2076
2346
|
"SECRET_DECRYPT_REFUSED",
|
|
2077
2347
|
`Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
|
|
2078
2348
|
"A secret with a plaintext value file is a `penv doctor` failure. Drop `secret` from the parameter's meta if it is not one, or run `penv generate --allow-decrypt` if you need the plaintext value in a `.env` artifact."
|
|
@@ -2080,7 +2350,7 @@ async function runDecrypt(options) {
|
|
|
2080
2350
|
}
|
|
2081
2351
|
const stored = await readOne(project, sealed);
|
|
2082
2352
|
if (stored === void 0) {
|
|
2083
|
-
throw new
|
|
2353
|
+
throw new PenvError8(
|
|
2084
2354
|
"PARAMETER_ABSENT",
|
|
2085
2355
|
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
2086
2356
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
@@ -2103,7 +2373,7 @@ async function runDecrypt(options) {
|
|
|
2103
2373
|
removed: formatValueFile4(sealed)
|
|
2104
2374
|
};
|
|
2105
2375
|
}
|
|
2106
|
-
var UndecryptableAt = class extends
|
|
2376
|
+
var UndecryptableAt = class extends PenvError8 {
|
|
2107
2377
|
constructor(parameter, environment, location, detail) {
|
|
2108
2378
|
super(
|
|
2109
2379
|
"VALUE_UNDECRYPTABLE",
|
|
@@ -2158,57 +2428,14 @@ var decryptCommand = defineCommand5({
|
|
|
2158
2428
|
});
|
|
2159
2429
|
|
|
2160
2430
|
// src/commands/fill.ts
|
|
2161
|
-
import { PenvError as
|
|
2431
|
+
import { PenvError as PenvError9 } from "@penvhq/core";
|
|
2162
2432
|
import { defineCommand as defineCommand6 } from "citty";
|
|
2163
|
-
|
|
2164
|
-
// src/input.ts
|
|
2165
|
-
import { createInterface } from "readline";
|
|
2166
|
-
function lineReader(input = process.stdin, output = process.stdout) {
|
|
2167
|
-
const rl = createInterface({ input, output });
|
|
2168
|
-
const buffered = [];
|
|
2169
|
-
const waiting = [];
|
|
2170
|
-
let closed = false;
|
|
2171
|
-
rl.on("line", (line) => {
|
|
2172
|
-
const next = waiting.shift();
|
|
2173
|
-
if (next === void 0) {
|
|
2174
|
-
buffered.push(line);
|
|
2175
|
-
} else {
|
|
2176
|
-
next(line);
|
|
2177
|
-
}
|
|
2178
|
-
});
|
|
2179
|
-
rl.on("close", () => {
|
|
2180
|
-
closed = true;
|
|
2181
|
-
for (const next of waiting.splice(0)) {
|
|
2182
|
-
next(void 0);
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
return {
|
|
2186
|
-
ask(question) {
|
|
2187
|
-
const early = buffered.shift();
|
|
2188
|
-
if (early !== void 0) {
|
|
2189
|
-
return Promise.resolve(early);
|
|
2190
|
-
}
|
|
2191
|
-
if (closed) {
|
|
2192
|
-
return Promise.resolve(void 0);
|
|
2193
|
-
}
|
|
2194
|
-
rl.setPrompt(question);
|
|
2195
|
-
rl.prompt();
|
|
2196
|
-
return new Promise((resolve7) => {
|
|
2197
|
-
waiting.push(resolve7);
|
|
2198
|
-
});
|
|
2199
|
-
},
|
|
2200
|
-
close() {
|
|
2201
|
-
rl.close();
|
|
2202
|
-
}
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// src/commands/fill.ts
|
|
2207
2433
|
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
2208
2434
|
async function runFill(options) {
|
|
2209
2435
|
const validation = await runValidate({
|
|
2210
2436
|
cwd: options.cwd,
|
|
2211
|
-
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
2437
|
+
...options.environment === void 0 ? {} : { environment: options.environment },
|
|
2438
|
+
...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
|
|
2212
2439
|
});
|
|
2213
2440
|
const environment = validation.environment;
|
|
2214
2441
|
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
@@ -2216,7 +2443,7 @@ async function runFill(options) {
|
|
|
2216
2443
|
const detail = blockers.map(
|
|
2217
2444
|
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
2218
2445
|
).join("\n");
|
|
2219
|
-
throw new
|
|
2446
|
+
throw new PenvError9(
|
|
2220
2447
|
"FILL_BLOCKED",
|
|
2221
2448
|
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
2222
2449
|
${detail}`,
|
|
@@ -2338,7 +2565,8 @@ var fillCommand = defineCommand6({
|
|
|
2338
2565
|
await runFill({
|
|
2339
2566
|
cwd: process.cwd(),
|
|
2340
2567
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2341
|
-
ask
|
|
2568
|
+
ask,
|
|
2569
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
2342
2570
|
})
|
|
2343
2571
|
)
|
|
2344
2572
|
);
|
|
@@ -2355,7 +2583,7 @@ import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
|
2355
2583
|
import {
|
|
2356
2584
|
checkNameCollisions as checkNameCollisions3,
|
|
2357
2585
|
effectiveMeta as effectiveMeta2,
|
|
2358
|
-
PenvError as
|
|
2586
|
+
PenvError as PenvError10,
|
|
2359
2587
|
requireValue as requireValue2,
|
|
2360
2588
|
serializeDotenv,
|
|
2361
2589
|
variableName as variableName4
|
|
@@ -2379,7 +2607,7 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2379
2607
|
const winner = resolution.winner;
|
|
2380
2608
|
if (winner?.file.encrypted === true) {
|
|
2381
2609
|
if (!allowDecrypt) {
|
|
2382
|
-
throw new
|
|
2610
|
+
throw new PenvError10(
|
|
2383
2611
|
"ENCRYPTED_VALUE_REFUSED",
|
|
2384
2612
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
2385
2613
|
`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.`
|
|
@@ -2403,12 +2631,12 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2403
2631
|
}
|
|
2404
2632
|
function generateDotenv(options) {
|
|
2405
2633
|
const project = openProject(options.cwd);
|
|
2406
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2634
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2407
2635
|
return serializeDotenv(entriesFor(project, environment, options.allowDecrypt === true).entries);
|
|
2408
2636
|
}
|
|
2409
2637
|
function runGenerate(options) {
|
|
2410
2638
|
const project = openProject(options.cwd);
|
|
2411
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2639
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2412
2640
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
2413
2641
|
const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
|
|
2414
2642
|
writeFileSync(file, serializeDotenv(entries), "utf8");
|
|
@@ -2454,7 +2682,8 @@ var generateCommand = defineCommand7({
|
|
|
2454
2682
|
cwd,
|
|
2455
2683
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2456
2684
|
...args.out === void 0 ? {} : { out: args.out },
|
|
2457
|
-
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
2685
|
+
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
|
|
2686
|
+
envFlags: shorthandCandidates(args, ["env", "out", "allow-decrypt", "allowDecrypt"])
|
|
2458
2687
|
});
|
|
2459
2688
|
write(renderGenerate(result2, cwd));
|
|
2460
2689
|
});
|
|
@@ -2462,7 +2691,7 @@ var generateCommand = defineCommand7({
|
|
|
2462
2691
|
});
|
|
2463
2692
|
|
|
2464
2693
|
// src/commands/get.ts
|
|
2465
|
-
import { PenvError as
|
|
2694
|
+
import { PenvError as PenvError11, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
|
|
2466
2695
|
import { defineCommand as defineCommand8 } from "citty";
|
|
2467
2696
|
async function runGet(options) {
|
|
2468
2697
|
const project = openProject(options.cwd);
|
|
@@ -2472,7 +2701,7 @@ async function runGet(options) {
|
|
|
2472
2701
|
const resolution = await resolveParameter(ref, environment, project.provider, keys);
|
|
2473
2702
|
const value = requireValue3(resolution, environment);
|
|
2474
2703
|
if (value === void 0) {
|
|
2475
|
-
throw new
|
|
2704
|
+
throw new PenvError11(
|
|
2476
2705
|
"PARAMETER_ABSENT",
|
|
2477
2706
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
2478
2707
|
`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.`
|
|
@@ -2559,7 +2788,7 @@ import {
|
|
|
2559
2788
|
isReservedToken as isReservedToken3,
|
|
2560
2789
|
loadConfigFrom as loadConfigFrom2,
|
|
2561
2790
|
lookupEnvironment,
|
|
2562
|
-
PenvError as
|
|
2791
|
+
PenvError as PenvError13,
|
|
2563
2792
|
parseDotenv,
|
|
2564
2793
|
ReservedTokenError as ReservedTokenError3,
|
|
2565
2794
|
refFromVariable,
|
|
@@ -2679,7 +2908,7 @@ import {
|
|
|
2679
2908
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
2680
2909
|
isLegalEnvironmentName,
|
|
2681
2910
|
loadConfigFrom,
|
|
2682
|
-
PenvError as
|
|
2911
|
+
PenvError as PenvError12,
|
|
2683
2912
|
RESERVED_TOKENS,
|
|
2684
2913
|
schemaFileOf as schemaFileOf2,
|
|
2685
2914
|
schemaInsideTree,
|
|
@@ -2723,7 +2952,7 @@ function suggestEnvironments(root) {
|
|
|
2723
2952
|
return [...found].sort();
|
|
2724
2953
|
}
|
|
2725
2954
|
function emptyFlag(flag) {
|
|
2726
|
-
return new
|
|
2955
|
+
return new PenvError12(
|
|
2727
2956
|
"INIT_FLAG_EMPTY",
|
|
2728
2957
|
`\`--${flag}\` was given without a value`,
|
|
2729
2958
|
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${DEFAULT_SCHEMA_FILE2}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
|
|
@@ -2786,7 +3015,7 @@ function planInit(root, flags = {}) {
|
|
|
2786
3015
|
throw emptyFlag("alias");
|
|
2787
3016
|
}
|
|
2788
3017
|
if (!ALIAS_NAME.test(alias)) {
|
|
2789
|
-
throw new
|
|
3018
|
+
throw new PenvError12(
|
|
2790
3019
|
"INIT_ALIAS_INVALID",
|
|
2791
3020
|
`\`${alias}\` is not an alias penv can write`,
|
|
2792
3021
|
`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.`
|
|
@@ -2882,6 +3111,15 @@ ${body}
|
|
|
2882
3111
|
// code. Importing it loads configuration and throws (naming the parameter and
|
|
2883
3112
|
// environment) if anything required is missing or invalid.
|
|
2884
3113
|
export const env = load(schema);
|
|
3114
|
+
|
|
3115
|
+
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3116
|
+
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3117
|
+
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
3118
|
+
declare module "@penvhq/core" {
|
|
3119
|
+
interface PenvSchemaShape {
|
|
3120
|
+
readonly shape: z.infer<typeof schema>;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
2885
3123
|
`;
|
|
2886
3124
|
}
|
|
2887
3125
|
function renderEnvironments(decisions) {
|
|
@@ -2891,14 +3129,14 @@ function renderEnvironments(decisions) {
|
|
|
2891
3129
|
// can read off your codebase, and an environment you do not have is worse
|
|
2892
3130
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
2893
3131
|
// environments: ["development", "production"],
|
|
2894
|
-
// providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
|
|
3132
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
|
|
2895
3133
|
environments: [],
|
|
2896
3134
|
|
|
2897
3135
|
providers: {},
|
|
2898
3136
|
`;
|
|
2899
3137
|
}
|
|
2900
3138
|
const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
|
|
2901
|
-
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "filesystem" },
|
|
3139
|
+
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
|
|
2902
3140
|
`).join("");
|
|
2903
3141
|
return `${shared} environments: [${names}],
|
|
2904
3142
|
|
|
@@ -3077,7 +3315,7 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
3077
3315
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
3078
3316
|
}
|
|
3079
3317
|
function shapeError(what, target, alias) {
|
|
3080
|
-
return new
|
|
3318
|
+
return new PenvError12(
|
|
3081
3319
|
"TSCONFIG_SHAPE",
|
|
3082
3320
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
3083
3321
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
@@ -3357,7 +3595,7 @@ function assertImportable(ref, variable) {
|
|
|
3357
3595
|
if (!ref.name.includes(".")) {
|
|
3358
3596
|
return;
|
|
3359
3597
|
}
|
|
3360
|
-
throw new
|
|
3598
|
+
throw new PenvError13(
|
|
3361
3599
|
"IMPORT_UNPARSEABLE_NAME",
|
|
3362
3600
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
3363
3601
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
@@ -3376,10 +3614,10 @@ function assertRoundTrips(ref, variable, config) {
|
|
|
3376
3614
|
return;
|
|
3377
3615
|
}
|
|
3378
3616
|
const generated = variableName5(ref, config);
|
|
3379
|
-
throw new
|
|
3617
|
+
throw new PenvError13(
|
|
3380
3618
|
"IMPORT_LOSSY_NAME",
|
|
3381
3619
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
3382
|
-
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`
|
|
3620
|
+
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`override\` block of penv.config.ts \u2014 \`override: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
|
|
3383
3621
|
);
|
|
3384
3622
|
}
|
|
3385
3623
|
function collisionsIn(refs, config) {
|
|
@@ -3460,7 +3698,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
3460
3698
|
if (value.length > 0) {
|
|
3461
3699
|
return value;
|
|
3462
3700
|
}
|
|
3463
|
-
throw new
|
|
3701
|
+
throw new PenvError13(
|
|
3464
3702
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
3465
3703
|
`\`--env\` for the import of ${source} names no environment`,
|
|
3466
3704
|
`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.`
|
|
@@ -3470,7 +3708,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
3470
3708
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
3471
3709
|
return;
|
|
3472
3710
|
}
|
|
3473
|
-
throw new
|
|
3711
|
+
throw new PenvError13(
|
|
3474
3712
|
"IMPORT_ENV_CONFLICT",
|
|
3475
3713
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
3476
3714
|
`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.`
|
|
@@ -3514,7 +3752,7 @@ function importDotenv(options) {
|
|
|
3514
3752
|
const cwd = resolve5(options.cwd);
|
|
3515
3753
|
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
3516
3754
|
if (!existsSync3(file)) {
|
|
3517
|
-
throw new
|
|
3755
|
+
throw new PenvError13(
|
|
3518
3756
|
"IMPORT_FILE_MISSING",
|
|
3519
3757
|
`There is no file at ${file} to import`,
|
|
3520
3758
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
@@ -3659,7 +3897,7 @@ var importCommand = defineCommand10({
|
|
|
3659
3897
|
|
|
3660
3898
|
// src/commands/key.ts
|
|
3661
3899
|
import { randomBytes } from "crypto";
|
|
3662
|
-
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as
|
|
3900
|
+
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError14 } from "@penvhq/core";
|
|
3663
3901
|
import { defineCommand as defineCommand11 } from "citty";
|
|
3664
3902
|
|
|
3665
3903
|
// src/keychain.ts
|
|
@@ -3692,7 +3930,7 @@ function runKeyCreate(options) {
|
|
|
3692
3930
|
const environment = targetEnvironment(project, options.environment);
|
|
3693
3931
|
const declared = project.config.keys?.[environment];
|
|
3694
3932
|
if (declared === void 0) {
|
|
3695
|
-
throw new
|
|
3933
|
+
throw new PenvError14(
|
|
3696
3934
|
"KEY_SOURCE_UNDECLARED",
|
|
3697
3935
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
3698
3936
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
@@ -3706,14 +3944,14 @@ function runKeyCreate(options) {
|
|
|
3706
3944
|
try {
|
|
3707
3945
|
existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
|
|
3708
3946
|
} catch (cause) {
|
|
3709
|
-
throw new
|
|
3947
|
+
throw new PenvError14(
|
|
3710
3948
|
"KEYCHAIN_UNAVAILABLE",
|
|
3711
3949
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
3712
3950
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
3713
3951
|
);
|
|
3714
3952
|
}
|
|
3715
3953
|
if (existing !== null) {
|
|
3716
|
-
throw new
|
|
3954
|
+
throw new PenvError14(
|
|
3717
3955
|
"KEY_EXISTS",
|
|
3718
3956
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
3719
3957
|
"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."
|
|
@@ -3872,7 +4110,7 @@ import {
|
|
|
3872
4110
|
formatMetaFile,
|
|
3873
4111
|
formatValueFile as formatValueFile5,
|
|
3874
4112
|
openValue as openValue4,
|
|
3875
|
-
PenvError as
|
|
4113
|
+
PenvError as PenvError15,
|
|
3876
4114
|
parameterId as parameterId5,
|
|
3877
4115
|
sealValue as sealValue3
|
|
3878
4116
|
} from "@penvhq/core";
|
|
@@ -3891,7 +4129,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
3891
4129
|
}
|
|
3892
4130
|
const environment = environmentOf2(source);
|
|
3893
4131
|
if (environment === void 0) {
|
|
3894
|
-
throw new
|
|
4132
|
+
throw new PenvError15(
|
|
3895
4133
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3896
4134
|
`${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3897
4135
|
"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."
|
|
@@ -3900,7 +4138,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
3900
4138
|
const keys = keySourceFor(project, environment);
|
|
3901
4139
|
const opened = openValue4(source, stored, keys);
|
|
3902
4140
|
if (opened.kind === "failed") {
|
|
3903
|
-
throw new
|
|
4141
|
+
throw new PenvError15(
|
|
3904
4142
|
"VALUE_UNDECRYPTABLE",
|
|
3905
4143
|
`${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3906
4144
|
"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."
|
|
@@ -3923,7 +4161,7 @@ async function runMove(options) {
|
|
|
3923
4161
|
assertWritableKey(options.to);
|
|
3924
4162
|
const to = refFromKey(options.to, project.config);
|
|
3925
4163
|
if (parameterId5(from) === parameterId5(to)) {
|
|
3926
|
-
throw new
|
|
4164
|
+
throw new PenvError15(
|
|
3927
4165
|
"PARAMETER_UNCHANGED",
|
|
3928
4166
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3929
4167
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3933,7 +4171,7 @@ async function runMove(options) {
|
|
|
3933
4171
|
const sources = filesOf(all, from);
|
|
3934
4172
|
const meta = await project.provider.readMeta(from);
|
|
3935
4173
|
if (sources.length === 0 && meta === void 0) {
|
|
3936
|
-
throw new
|
|
4174
|
+
throw new PenvError15(
|
|
3937
4175
|
"PARAMETER_ABSENT",
|
|
3938
4176
|
`Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
|
|
3939
4177
|
`\`penv list\` shows every parameter penv holds.`
|
|
@@ -3941,7 +4179,7 @@ async function runMove(options) {
|
|
|
3941
4179
|
}
|
|
3942
4180
|
const occupied2 = filesOf(all, to);
|
|
3943
4181
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3944
|
-
throw new
|
|
4182
|
+
throw new PenvError15(
|
|
3945
4183
|
"PARAMETER_EXISTS",
|
|
3946
4184
|
`Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
|
|
3947
4185
|
`Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
@@ -4023,14 +4261,45 @@ var mvCommand = defineCommand13({
|
|
|
4023
4261
|
});
|
|
4024
4262
|
|
|
4025
4263
|
// src/commands/pull.ts
|
|
4264
|
+
import { holdsProjection as holdsProjection4, refFromVariable as refFromVariable2 } from "@penvhq/core";
|
|
4026
4265
|
import { defineCommand as defineCommand14 } from "citty";
|
|
4266
|
+
async function pullProjection(project, source, environment) {
|
|
4267
|
+
const tree = localTree(project);
|
|
4268
|
+
const names = /* @__PURE__ */ new Set();
|
|
4269
|
+
for (const secret of await source.list({ kind: "repository" })) {
|
|
4270
|
+
names.add(secret.name);
|
|
4271
|
+
}
|
|
4272
|
+
for (const secret of await source.list({ kind: "environment", environment })) {
|
|
4273
|
+
names.add(secret.name);
|
|
4274
|
+
}
|
|
4275
|
+
let meta = 0;
|
|
4276
|
+
for (const name of [...names].sort()) {
|
|
4277
|
+
const ref = refFromVariable2(name);
|
|
4278
|
+
if (tree.readMetaSync(ref) === void 0) {
|
|
4279
|
+
tree.writeMetaSync(ref, {});
|
|
4280
|
+
meta += 1;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
return {
|
|
4284
|
+
environment,
|
|
4285
|
+
source: source.type,
|
|
4286
|
+
localSource: false,
|
|
4287
|
+
values: 0,
|
|
4288
|
+
meta,
|
|
4289
|
+
refs: names.size,
|
|
4290
|
+
valuesUnreadable: true
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4027
4293
|
async function runPull(options) {
|
|
4028
4294
|
const project = openProject(options.cwd);
|
|
4029
|
-
const environment = targetEnvironment(project, options.environment);
|
|
4030
|
-
const source = await sourceProviderFor(project, environment);
|
|
4295
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
4296
|
+
const source = options.source ?? await sourceProviderFor(project, environment);
|
|
4031
4297
|
if (source.type === LOCAL_TREE_TYPE) {
|
|
4032
4298
|
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
4033
4299
|
}
|
|
4300
|
+
if (holdsProjection4(source)) {
|
|
4301
|
+
return pullProjection(project, source, environment);
|
|
4302
|
+
}
|
|
4034
4303
|
const tree = localTree(project);
|
|
4035
4304
|
const files = await source.list();
|
|
4036
4305
|
let values = 0;
|
|
@@ -4065,6 +4334,22 @@ function renderPull(result2) {
|
|
|
4065
4334
|
}
|
|
4066
4335
|
]);
|
|
4067
4336
|
}
|
|
4337
|
+
if (result2.valuesUnreadable === true) {
|
|
4338
|
+
return formatRows([
|
|
4339
|
+
{
|
|
4340
|
+
glyph: CHECK,
|
|
4341
|
+
label: "Pulled",
|
|
4342
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter name" : "parameter names"}`,
|
|
4343
|
+
detail: `from ${result2.source} for environment ${result2.environment}`
|
|
4344
|
+
},
|
|
4345
|
+
{
|
|
4346
|
+
glyph: WARN,
|
|
4347
|
+
label: "Values not readable",
|
|
4348
|
+
subject: "this destination never returns a secret's value",
|
|
4349
|
+
detail: "fill them locally with `penv set` or `penv fill` \u2014 `penv validate` names every gap"
|
|
4350
|
+
}
|
|
4351
|
+
]);
|
|
4352
|
+
}
|
|
4068
4353
|
return formatRows([
|
|
4069
4354
|
{
|
|
4070
4355
|
glyph: CHECK,
|
|
@@ -4086,13 +4371,17 @@ var pullCommand = defineCommand14({
|
|
|
4086
4371
|
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
4087
4372
|
},
|
|
4088
4373
|
args: {
|
|
4089
|
-
env: {
|
|
4374
|
+
env: {
|
|
4375
|
+
type: "string",
|
|
4376
|
+
description: "The environment to pull (or pass it as a bare flag: --production)"
|
|
4377
|
+
}
|
|
4090
4378
|
},
|
|
4091
4379
|
run({ args }) {
|
|
4092
4380
|
return guard(async () => {
|
|
4093
4381
|
const result2 = await runPull({
|
|
4094
4382
|
cwd: process.cwd(),
|
|
4095
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
4383
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4384
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
4096
4385
|
});
|
|
4097
4386
|
write(renderPull(result2));
|
|
4098
4387
|
});
|
|
@@ -4175,7 +4464,8 @@ var removeCommand = defineCommand15({
|
|
|
4175
4464
|
import {
|
|
4176
4465
|
beginRotation,
|
|
4177
4466
|
completeRotation,
|
|
4178
|
-
|
|
4467
|
+
holdsRecords as holdsRecords2,
|
|
4468
|
+
PenvError as PenvError16,
|
|
4179
4469
|
retainsPrevious,
|
|
4180
4470
|
rotationOf as rotationOf2
|
|
4181
4471
|
} from "@penvhq/core";
|
|
@@ -4204,7 +4494,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
4204
4494
|
}
|
|
4205
4495
|
function requireNewValue(value, phase, key) {
|
|
4206
4496
|
if (value === void 0) {
|
|
4207
|
-
throw new
|
|
4497
|
+
throw new PenvError16(
|
|
4208
4498
|
"ROTATION_NO_VALUE",
|
|
4209
4499
|
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
4210
4500
|
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
@@ -4214,7 +4504,7 @@ function requireNewValue(value, phase, key) {
|
|
|
4214
4504
|
}
|
|
4215
4505
|
function requireRetaining(provider, environment) {
|
|
4216
4506
|
if (!retainsPrevious(provider)) {
|
|
4217
|
-
throw new
|
|
4507
|
+
throw new PenvError16(
|
|
4218
4508
|
"ROTATION_NOT_RETAINING",
|
|
4219
4509
|
`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`,
|
|
4220
4510
|
"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."
|
|
@@ -4226,12 +4516,20 @@ async function runRotate(options) {
|
|
|
4226
4516
|
const project = openProject(options.cwd);
|
|
4227
4517
|
const environment = targetEnvironment(project, options.environment);
|
|
4228
4518
|
const ref = refFromKey(options.key, project.config);
|
|
4229
|
-
const
|
|
4519
|
+
const source = await sourceProviderFor(project, environment);
|
|
4520
|
+
if (!holdsRecords2(source)) {
|
|
4521
|
+
throw new PenvError16(
|
|
4522
|
+
"ROTATION_NOT_RECORDS",
|
|
4523
|
+
`Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
|
|
4524
|
+
"Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
|
|
4525
|
+
);
|
|
4526
|
+
}
|
|
4527
|
+
const provider = source;
|
|
4230
4528
|
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4231
4529
|
const before = await provider.readMeta(ref);
|
|
4232
4530
|
const { mechanism } = rotationOf2(before, environment);
|
|
4233
4531
|
if (mechanism === void 0) {
|
|
4234
|
-
throw new
|
|
4532
|
+
throw new PenvError16(
|
|
4235
4533
|
"ROTATION_NO_MECHANISM",
|
|
4236
4534
|
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
4237
4535
|
'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.'
|
|
@@ -4241,7 +4539,7 @@ async function runRotate(options) {
|
|
|
4241
4539
|
const complete = options.complete === true;
|
|
4242
4540
|
if (mechanism === "atomic-cutover") {
|
|
4243
4541
|
if (begin || complete) {
|
|
4244
|
-
throw new
|
|
4542
|
+
throw new PenvError16(
|
|
4245
4543
|
"ROTATION_MECHANISM_MISMATCH",
|
|
4246
4544
|
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
4247
4545
|
"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."
|
|
@@ -4255,7 +4553,7 @@ async function runRotate(options) {
|
|
|
4255
4553
|
}
|
|
4256
4554
|
const retaining = requireRetaining(provider, environment);
|
|
4257
4555
|
if (begin === complete) {
|
|
4258
|
-
throw new
|
|
4556
|
+
throw new PenvError16(
|
|
4259
4557
|
"ROTATION_PHASE_REQUIRED",
|
|
4260
4558
|
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
4261
4559
|
"`--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."
|