@penvhq/cli 0.3.2 → 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 +1009 -423
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -29
- package/dist/index.d.ts +90 -29
- package/dist/index.js +862 -267
- 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."
|
|
@@ -376,29 +420,61 @@ function permitsAbsence(node) {
|
|
|
376
420
|
}
|
|
377
421
|
return void 0;
|
|
378
422
|
}
|
|
379
|
-
function leaves(node, path, inherited,
|
|
423
|
+
function leaves(node, path, inherited, out2) {
|
|
380
424
|
const own = permitsAbsence(node);
|
|
381
425
|
const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
|
|
382
426
|
const shape = shapeOf(unwrap(node));
|
|
383
427
|
if (shape === void 0) {
|
|
384
428
|
if (path.length > 0) {
|
|
385
|
-
|
|
429
|
+
out2.push({ path, absencePermitted, node });
|
|
386
430
|
}
|
|
387
431
|
return;
|
|
388
432
|
}
|
|
389
433
|
for (const key of Object.keys(shape)) {
|
|
390
|
-
leaves(shape[key], [...path, key], absencePermitted,
|
|
434
|
+
leaves(shape[key], [...path, key], absencePermitted, out2);
|
|
391
435
|
}
|
|
392
436
|
}
|
|
393
437
|
function combine(left, right) {
|
|
394
438
|
return left === void 0 || right === void 0 ? void 0 : false;
|
|
395
439
|
}
|
|
396
440
|
function declaredLeaves(schema) {
|
|
397
|
-
const
|
|
398
|
-
leaves(schema, [], false,
|
|
399
|
-
return
|
|
441
|
+
const out2 = [];
|
|
442
|
+
leaves(schema, [], false, out2);
|
|
443
|
+
return out2;
|
|
444
|
+
}
|
|
445
|
+
function defaultValueOf(node) {
|
|
446
|
+
let current = node;
|
|
447
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
448
|
+
const def = defOf(current);
|
|
449
|
+
if (def === void 0) {
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
if (typeOf(current) === "default" || typeOf(current) === "prefault") {
|
|
453
|
+
let value = def.defaultValue;
|
|
454
|
+
if (typeof value === "function") {
|
|
455
|
+
try {
|
|
456
|
+
value = value();
|
|
457
|
+
} catch {
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (typeof value === "string") {
|
|
462
|
+
return JSON.stringify(value);
|
|
463
|
+
}
|
|
464
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
465
|
+
return String(value);
|
|
466
|
+
}
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
const inner = def.innerType;
|
|
470
|
+
if (inner === void 0) {
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
current = inner;
|
|
474
|
+
}
|
|
475
|
+
return void 0;
|
|
400
476
|
}
|
|
401
|
-
var EMPTY_DRIFT = { declared: [], undeclared: [] };
|
|
477
|
+
var EMPTY_DRIFT = { declared: [], undeclared: [], optional: [] };
|
|
402
478
|
function hasValue(resolution) {
|
|
403
479
|
return resolution.winner !== void 0;
|
|
404
480
|
}
|
|
@@ -406,23 +482,39 @@ function computeDrift(input) {
|
|
|
406
482
|
const { schema, resolutions, config, environment } = input;
|
|
407
483
|
const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
|
|
408
484
|
const declared = [];
|
|
485
|
+
const optional = [];
|
|
409
486
|
for (const leaf of declaredLeaves(schema)) {
|
|
410
|
-
if (leaf.absencePermitted
|
|
487
|
+
if (leaf.absencePermitted === void 0) {
|
|
411
488
|
continue;
|
|
412
489
|
}
|
|
413
490
|
const path = leaf.path.join(".");
|
|
414
491
|
const ref = refFromAccessPath2(leaf.path);
|
|
492
|
+
const renameRemedy = `Rename the \`${path}\` key in .penv/env.ts \u2014 a parameter name is lower-case, hyphenated, and never a reserved token, so no value file reaches this key.`;
|
|
415
493
|
if (ref === void 0 || isReservedToken2(ref.name, config)) {
|
|
416
|
-
|
|
417
|
-
subject: path,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
494
|
+
if (leaf.absencePermitted) {
|
|
495
|
+
optional.push({ subject: path, remedy: renameRemedy });
|
|
496
|
+
} else {
|
|
497
|
+
declared.push({
|
|
498
|
+
subject: path,
|
|
499
|
+
remedy: renameRemedy,
|
|
500
|
+
detail: "declared, no filename reaches it"
|
|
501
|
+
});
|
|
502
|
+
}
|
|
421
503
|
continue;
|
|
422
504
|
}
|
|
423
505
|
if (valued.has(parameterId2(ref))) {
|
|
424
506
|
continue;
|
|
425
507
|
}
|
|
508
|
+
if (leaf.absencePermitted) {
|
|
509
|
+
const defaultValue = defaultValueOf(leaf.node);
|
|
510
|
+
optional.push({
|
|
511
|
+
subject: parameterId2(ref),
|
|
512
|
+
ref,
|
|
513
|
+
...defaultValue === void 0 ? {} : { defaultValue },
|
|
514
|
+
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
426
518
|
declared.push({
|
|
427
519
|
subject: parameterId2(ref),
|
|
428
520
|
ref,
|
|
@@ -440,28 +532,91 @@ function computeDrift(input) {
|
|
|
440
532
|
variable: variableName(resolution.ref, config)
|
|
441
533
|
});
|
|
442
534
|
}
|
|
443
|
-
return { declared, undeclared };
|
|
535
|
+
return { declared, undeclared, optional };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/style.ts
|
|
539
|
+
function supportsColor(stream) {
|
|
540
|
+
const env = process.env;
|
|
541
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
if (env.TERM === "dumb") {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
return stream.isTTY === true;
|
|
551
|
+
}
|
|
552
|
+
var ESC = `${String.fromCharCode(27)}[`;
|
|
553
|
+
var identity = (text) => text;
|
|
554
|
+
function sgr(open, close, on) {
|
|
555
|
+
if (!on) {
|
|
556
|
+
return identity;
|
|
557
|
+
}
|
|
558
|
+
const opener = `${ESC}${open}m`;
|
|
559
|
+
const closer = `${ESC}${close}m`;
|
|
560
|
+
return (text) => `${opener}${text}${closer}`;
|
|
561
|
+
}
|
|
562
|
+
function paletteFor(stream) {
|
|
563
|
+
const on = supportsColor(stream);
|
|
564
|
+
return {
|
|
565
|
+
enabled: on,
|
|
566
|
+
bold: sgr(1, 22, on),
|
|
567
|
+
dim: sgr(2, 22, on),
|
|
568
|
+
red: sgr(31, 39, on),
|
|
569
|
+
green: sgr(32, 39, on),
|
|
570
|
+
yellow: sgr(33, 39, on),
|
|
571
|
+
cyan: sgr(36, 39, on),
|
|
572
|
+
magenta: sgr(35, 39, on)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
var out = paletteFor(process.stdout);
|
|
576
|
+
var err = paletteFor(process.stderr);
|
|
577
|
+
var STYLE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
578
|
+
function visibleWidth(text) {
|
|
579
|
+
return text.replace(STYLE_PATTERN, "").length;
|
|
580
|
+
}
|
|
581
|
+
function padVisible(text, width) {
|
|
582
|
+
const missing = width - visibleWidth(text);
|
|
583
|
+
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
444
584
|
}
|
|
445
585
|
|
|
446
586
|
// src/ui.ts
|
|
447
|
-
import { PenvError as
|
|
587
|
+
import { PenvError as PenvError4 } from "@penvhq/core";
|
|
448
588
|
var CHECK = "\u2713";
|
|
449
589
|
var WARN = "\u26A0";
|
|
590
|
+
var CROSS = "\u2717";
|
|
450
591
|
var UNKNOWN = "?";
|
|
592
|
+
function paintGlyph(glyph) {
|
|
593
|
+
switch (glyph) {
|
|
594
|
+
case CHECK:
|
|
595
|
+
return out.green(glyph);
|
|
596
|
+
case WARN:
|
|
597
|
+
return out.yellow(glyph);
|
|
598
|
+
case CROSS:
|
|
599
|
+
return out.red(glyph);
|
|
600
|
+
case UNKNOWN:
|
|
601
|
+
return out.dim(glyph);
|
|
602
|
+
default:
|
|
603
|
+
return glyph;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
451
606
|
var NOTE_COLUMN = 29;
|
|
452
607
|
function widest(values) {
|
|
453
|
-
return values.reduce((max, value) => Math.max(max, value
|
|
608
|
+
return values.reduce((max, value) => Math.max(max, visibleWidth(value)), 0);
|
|
454
609
|
}
|
|
455
610
|
function formatRows(rows) {
|
|
456
611
|
const labelWidth = widest(rows.map((row) => row.label)) + 2;
|
|
457
612
|
const detailed = rows.filter((row) => row.detail !== void 0);
|
|
458
613
|
const subjectWidth = widest(detailed.map((row) => row.subject ?? "")) + 1;
|
|
459
614
|
return rows.map((row) => {
|
|
460
|
-
const head = `${row.glyph} ${row.label
|
|
615
|
+
const head = `${paintGlyph(row.glyph)} ${padVisible(row.label, labelWidth)}`;
|
|
461
616
|
if (row.detail === void 0) {
|
|
462
617
|
return `${head}${row.subject ?? ""}`.trimEnd();
|
|
463
618
|
}
|
|
464
|
-
return `${head}${(row.subject ?? ""
|
|
619
|
+
return `${head}${padVisible(row.subject ?? "", subjectWidth)}${out.dim(row.detail)}`.trimEnd();
|
|
465
620
|
});
|
|
466
621
|
}
|
|
467
622
|
function columns(rows, gap = 2) {
|
|
@@ -471,18 +626,31 @@ function columns(rows, gap = 2) {
|
|
|
471
626
|
widths.push(widest(rows.map((row) => row[column] ?? "")) + gap);
|
|
472
627
|
}
|
|
473
628
|
return rows.map(
|
|
474
|
-
(row) => row.map(
|
|
629
|
+
(row) => row.map(
|
|
630
|
+
(cell, index) => index === row.length - 1 ? cell : padVisible(cell, widths[index] ?? 0)
|
|
631
|
+
).join("").trimEnd()
|
|
475
632
|
);
|
|
476
633
|
}
|
|
477
634
|
function formatSteps(steps) {
|
|
478
635
|
return steps.map((step) => {
|
|
479
636
|
if (step.note === void 0) {
|
|
480
|
-
return `${step.glyph} ${step.text}`;
|
|
637
|
+
return `${paintGlyph(step.glyph)} ${step.text}`;
|
|
481
638
|
}
|
|
482
|
-
const text = step.text
|
|
483
|
-
return `${step.glyph} ${text}${step.note}`;
|
|
639
|
+
const text = visibleWidth(step.text) >= NOTE_COLUMN ? `${step.text} ` : padVisible(step.text, NOTE_COLUMN);
|
|
640
|
+
return `${paintGlyph(step.glyph)} ${text}${out.dim(step.note)}`;
|
|
484
641
|
});
|
|
485
642
|
}
|
|
643
|
+
function heading(command, context) {
|
|
644
|
+
return context === void 0 ? out.bold(command) : `${out.bold(command)} ${out.dim(`\xB7 ${context}`)}`;
|
|
645
|
+
}
|
|
646
|
+
function tip(text) {
|
|
647
|
+
return ` ${out.cyan("\u2192")} ${text}`;
|
|
648
|
+
}
|
|
649
|
+
function prompt(subject, context) {
|
|
650
|
+
const head = `${out.cyan("?")} ${out.bold(subject)}`;
|
|
651
|
+
const tail = out.dim("\u203A ");
|
|
652
|
+
return context === void 0 ? `${head} ${tail}` : `${head} ${out.dim(`\xB7 ${context}`)} ${tail}`;
|
|
653
|
+
}
|
|
486
654
|
function write(lines) {
|
|
487
655
|
for (const line of lines) {
|
|
488
656
|
process.stdout.write(`${line}
|
|
@@ -490,14 +658,27 @@ function write(lines) {
|
|
|
490
658
|
}
|
|
491
659
|
}
|
|
492
660
|
function reportError(error) {
|
|
493
|
-
if (error instanceof
|
|
494
|
-
|
|
661
|
+
if (error instanceof PenvError4) {
|
|
662
|
+
const suffix = error.remedy === void 0 ? void 0 : `
|
|
663
|
+
${error.remedy}`;
|
|
664
|
+
const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
|
|
665
|
+
process.stderr.write(`${err.red(CROSS)} ${message}
|
|
666
|
+
`);
|
|
667
|
+
if (error.remedy !== void 0) {
|
|
668
|
+
process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
|
|
495
669
|
`);
|
|
670
|
+
}
|
|
496
671
|
} else if (error instanceof Error) {
|
|
497
|
-
process.stderr.write(`${
|
|
672
|
+
process.stderr.write(`${err.red(CROSS)} ${error.message}
|
|
673
|
+
`);
|
|
674
|
+
const stack = error.stack;
|
|
675
|
+
const frames = stack?.startsWith(`${error.name}: ${error.message}`) ? stack.slice(`${error.name}: ${error.message}`.length).replace(/^\n/, "") : stack;
|
|
676
|
+
if (frames !== void 0 && frames !== "") {
|
|
677
|
+
process.stderr.write(`${err.dim(frames)}
|
|
498
678
|
`);
|
|
679
|
+
}
|
|
499
680
|
} else {
|
|
500
|
-
process.stderr.write(`${String(error)}
|
|
681
|
+
process.stderr.write(`${err.red(CROSS)} ${String(error)}
|
|
501
682
|
`);
|
|
502
683
|
}
|
|
503
684
|
process.exitCode = 1;
|
|
@@ -511,31 +692,92 @@ async function guard(run) {
|
|
|
511
692
|
}
|
|
512
693
|
|
|
513
694
|
// src/commands/push.ts
|
|
514
|
-
import {
|
|
515
|
-
|
|
695
|
+
import {
|
|
696
|
+
checkNameCollisions,
|
|
697
|
+
holdsProjection as holdsProjection2,
|
|
698
|
+
PenvError as PenvError5,
|
|
699
|
+
requireValue,
|
|
700
|
+
variableName as variableName2
|
|
701
|
+
} from "@penvhq/core";
|
|
516
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
|
|
517
747
|
var LAST_PUSHED_KEY = "lastPushedAt";
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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>\`.`
|
|
525
769
|
);
|
|
526
770
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return { sink: override, repo };
|
|
771
|
+
if (options.provider !== void 0) {
|
|
772
|
+
return { provider: options.provider, location };
|
|
530
773
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
);
|
|
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 };
|
|
539
781
|
}
|
|
540
782
|
function plan(resolutions, config, environment, allowDecrypt) {
|
|
541
783
|
const outbound = [];
|
|
@@ -547,10 +789,10 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
547
789
|
let encrypted = false;
|
|
548
790
|
if (winner.file.encrypted) {
|
|
549
791
|
if (!allowDecrypt) {
|
|
550
|
-
throw new
|
|
792
|
+
throw new PenvError5(
|
|
551
793
|
"ENCRYPTED_VALUE_REFUSED",
|
|
552
|
-
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for
|
|
553
|
-
"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."
|
|
554
796
|
);
|
|
555
797
|
}
|
|
556
798
|
requireValue(resolution, environment);
|
|
@@ -580,10 +822,46 @@ function recordPush(tree, ref, environment, iso) {
|
|
|
580
822
|
const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
|
|
581
823
|
tree.writeMetaSync(ref, meta);
|
|
582
824
|
}
|
|
583
|
-
async function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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) {
|
|
587
865
|
const tree = localTree(project);
|
|
588
866
|
const keys = keySourceFor(project, environment);
|
|
589
867
|
const resolutions = resolveAllSync(tree, environment, keys, true);
|
|
@@ -592,16 +870,17 @@ async function runPush(options) {
|
|
|
592
870
|
if (collision !== void 0) {
|
|
593
871
|
throw collision;
|
|
594
872
|
}
|
|
595
|
-
const nameError =
|
|
873
|
+
const nameError = provider.checkNames?.(refs, project.config)[0];
|
|
596
874
|
if (nameError !== void 0) {
|
|
597
875
|
throw nameError;
|
|
598
876
|
}
|
|
599
877
|
const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
|
|
600
|
-
await
|
|
878
|
+
await provider.verify();
|
|
879
|
+
const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
|
|
601
880
|
let repositorySecrets = 0;
|
|
602
881
|
let environmentSecrets = 0;
|
|
603
882
|
for (const item of outbound) {
|
|
604
|
-
await
|
|
883
|
+
await provider.push(item.variable, item.value, item.scope);
|
|
605
884
|
if (item.scope.kind === "repository") {
|
|
606
885
|
repositorySecrets += 1;
|
|
607
886
|
} else {
|
|
@@ -611,15 +890,70 @@ async function runPush(options) {
|
|
|
611
890
|
}
|
|
612
891
|
return {
|
|
613
892
|
environment,
|
|
614
|
-
|
|
893
|
+
destination: provider.type,
|
|
894
|
+
mode: "projection",
|
|
895
|
+
location,
|
|
615
896
|
pushed: outbound.length,
|
|
897
|
+
meta: 0,
|
|
616
898
|
repositorySecrets,
|
|
617
899
|
environmentSecrets,
|
|
618
|
-
decrypted: outbound.filter((item) => item.encrypted).length
|
|
900
|
+
decrypted: outbound.filter((item) => item.encrypted).length,
|
|
901
|
+
createdTarget
|
|
619
902
|
};
|
|
620
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
|
|
944
|
+
};
|
|
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
|
+
}
|
|
621
955
|
function renderPush(result2) {
|
|
622
|
-
if (result2.pushed === 0) {
|
|
956
|
+
if (result2.pushed === 0 && result2.meta === 0) {
|
|
623
957
|
return formatRows([
|
|
624
958
|
{
|
|
625
959
|
glyph: CHECK,
|
|
@@ -628,7 +962,23 @@ function renderPush(result2) {
|
|
|
628
962
|
}
|
|
629
963
|
]);
|
|
630
964
|
}
|
|
631
|
-
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
|
+
}
|
|
632
982
|
const rows = [
|
|
633
983
|
{
|
|
634
984
|
glyph: CHECK,
|
|
@@ -643,12 +993,20 @@ function renderPush(result2) {
|
|
|
643
993
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
644
994
|
}
|
|
645
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
|
+
}
|
|
646
1004
|
if (result2.decrypted > 0) {
|
|
647
1005
|
rows.push({
|
|
648
1006
|
glyph: WARN,
|
|
649
1007
|
label: "Decrypted",
|
|
650
1008
|
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
651
|
-
detail: "sent as plaintext for
|
|
1009
|
+
detail: "sent as plaintext for the destination to re-seal under its own key"
|
|
652
1010
|
});
|
|
653
1011
|
}
|
|
654
1012
|
return formatRows(rows);
|
|
@@ -656,13 +1014,28 @@ function renderPush(result2) {
|
|
|
656
1014
|
var pushCommand = defineCommand({
|
|
657
1015
|
meta: {
|
|
658
1016
|
name: "push",
|
|
659
|
-
description: "Push an environment's
|
|
1017
|
+
description: "Push an environment's values to its provider"
|
|
660
1018
|
},
|
|
661
1019
|
args: {
|
|
662
1020
|
env: { type: "string", description: "The environment to push" },
|
|
663
1021
|
"allow-decrypt": {
|
|
664
1022
|
type: "boolean",
|
|
665
|
-
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"
|
|
666
1039
|
}
|
|
667
1040
|
},
|
|
668
1041
|
run({ args }) {
|
|
@@ -670,7 +1043,22 @@ var pushCommand = defineCommand({
|
|
|
670
1043
|
const result2 = await runPush({
|
|
671
1044
|
cwd: process.cwd(),
|
|
672
1045
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
673
|
-
...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
|
+
])
|
|
674
1062
|
});
|
|
675
1063
|
write(renderPush(result2));
|
|
676
1064
|
});
|
|
@@ -685,7 +1073,7 @@ import {
|
|
|
685
1073
|
checkNameCollisions as checkNameCollisions2,
|
|
686
1074
|
jitiFor,
|
|
687
1075
|
NameCollisionError,
|
|
688
|
-
PenvError as
|
|
1076
|
+
PenvError as PenvError6,
|
|
689
1077
|
ReservedTokenError as ReservedTokenError2,
|
|
690
1078
|
resolveAll,
|
|
691
1079
|
SCHEMA_HARVEST_ENV,
|
|
@@ -824,14 +1212,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
824
1212
|
}
|
|
825
1213
|
async function runValidate(options) {
|
|
826
1214
|
const project = openProject(options.cwd);
|
|
827
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1215
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
828
1216
|
const schemaPath = schemaFileOf(project.config);
|
|
829
1217
|
const issues = [];
|
|
830
1218
|
let files;
|
|
831
1219
|
try {
|
|
832
1220
|
files = await project.provider.list();
|
|
833
1221
|
} catch (error) {
|
|
834
|
-
if (!(error instanceof
|
|
1222
|
+
if (!(error instanceof PenvError6)) {
|
|
835
1223
|
throw error;
|
|
836
1224
|
}
|
|
837
1225
|
return {
|
|
@@ -905,18 +1293,26 @@ function renderValidate(result2) {
|
|
|
905
1293
|
]);
|
|
906
1294
|
}
|
|
907
1295
|
const rows = result2.issues.map((issue) => ({
|
|
908
|
-
glyph:
|
|
1296
|
+
glyph: CROSS,
|
|
909
1297
|
label: LABELS[issue.kind],
|
|
910
1298
|
subject: issue.subject,
|
|
911
1299
|
detail: issue.message
|
|
912
1300
|
}));
|
|
913
1301
|
const lines = formatRows(rows);
|
|
914
|
-
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))]
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1302
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))].filter(
|
|
1303
|
+
(remedy) => remedy !== void 0
|
|
1304
|
+
);
|
|
1305
|
+
if (remedies.length > 0) {
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
for (const remedy of remedies) {
|
|
1308
|
+
lines.push(tip(remedy));
|
|
918
1309
|
}
|
|
919
1310
|
}
|
|
1311
|
+
const count = result2.issues.length;
|
|
1312
|
+
lines.push(
|
|
1313
|
+
"",
|
|
1314
|
+
`${out.red(CROSS)} ${count} ${count === 1 ? "issue" : "issues"} for environment ${result2.environment}`
|
|
1315
|
+
);
|
|
920
1316
|
return lines;
|
|
921
1317
|
}
|
|
922
1318
|
var validateCommand = defineCommand2({
|
|
@@ -931,7 +1327,8 @@ var validateCommand = defineCommand2({
|
|
|
931
1327
|
return guard(async () => {
|
|
932
1328
|
const result2 = await runValidate({
|
|
933
1329
|
cwd: process.cwd(),
|
|
934
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
1330
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
1331
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
935
1332
|
});
|
|
936
1333
|
write(renderValidate(result2));
|
|
937
1334
|
if (!result2.ok) {
|
|
@@ -954,10 +1351,19 @@ function skipped(check, label) {
|
|
|
954
1351
|
}
|
|
955
1352
|
async function runDoctor(options) {
|
|
956
1353
|
const project = openProject(options.cwd);
|
|
957
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1354
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
958
1355
|
const findings = [];
|
|
959
1356
|
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
960
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
|
+
}
|
|
961
1367
|
const { schema, issues } = await loadSchema(project, environment);
|
|
962
1368
|
if (schema !== void 0) {
|
|
963
1369
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -1014,7 +1420,7 @@ async function runDoctor(options) {
|
|
|
1014
1420
|
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1015
1421
|
}
|
|
1016
1422
|
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
1017
|
-
findings.push(...await
|
|
1423
|
+
findings.push(...await projectionFindings(project, environment, options.projection));
|
|
1018
1424
|
findings.push({
|
|
1019
1425
|
check: "provider",
|
|
1020
1426
|
severity: "pass",
|
|
@@ -1250,15 +1656,6 @@ function encryptionFindings(subjects, environment) {
|
|
|
1250
1656
|
];
|
|
1251
1657
|
}
|
|
1252
1658
|
var EDIT_SKEW_MS = 12e4;
|
|
1253
|
-
function buildSink(declared, override) {
|
|
1254
|
-
if (override !== void 0) {
|
|
1255
|
-
return override;
|
|
1256
|
-
}
|
|
1257
|
-
if (declared.type === "github") {
|
|
1258
|
-
return createGithubSink2(declared.repo === void 0 ? {} : { repo: declared.repo });
|
|
1259
|
-
}
|
|
1260
|
-
return void 0;
|
|
1261
|
-
}
|
|
1262
1659
|
function errorDetail(error) {
|
|
1263
1660
|
if (error instanceof Error) {
|
|
1264
1661
|
return error.message.split("\n")[0] ?? error.message;
|
|
@@ -1268,32 +1665,43 @@ function errorDetail(error) {
|
|
|
1268
1665
|
function scopeLabel(scope) {
|
|
1269
1666
|
return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
|
|
1270
1667
|
}
|
|
1271
|
-
async function
|
|
1272
|
-
|
|
1273
|
-
if (
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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;
|
|
1287
1695
|
}
|
|
1288
1696
|
try {
|
|
1289
|
-
await
|
|
1697
|
+
await projection.verify();
|
|
1290
1698
|
} catch (error) {
|
|
1291
1699
|
return [
|
|
1292
1700
|
{
|
|
1293
|
-
check: "
|
|
1701
|
+
check: "projection-unreachable",
|
|
1294
1702
|
severity: "unknown",
|
|
1295
|
-
label: "
|
|
1296
|
-
subject: `could not reach the ${
|
|
1703
|
+
label: "Destination",
|
|
1704
|
+
subject: `could not reach the ${projection.type} destination for ${environment}`,
|
|
1297
1705
|
detail: errorDetail(error)
|
|
1298
1706
|
}
|
|
1299
1707
|
];
|
|
@@ -1301,15 +1709,15 @@ async function sinkFindings(project, environment, override) {
|
|
|
1301
1709
|
let repoSecrets;
|
|
1302
1710
|
let envSecrets;
|
|
1303
1711
|
try {
|
|
1304
|
-
repoSecrets = await
|
|
1305
|
-
envSecrets = await
|
|
1712
|
+
repoSecrets = await projection.list({ kind: "repository" });
|
|
1713
|
+
envSecrets = await projection.list({ kind: "environment", environment });
|
|
1306
1714
|
} catch (error) {
|
|
1307
1715
|
return [
|
|
1308
1716
|
{
|
|
1309
|
-
check: "
|
|
1717
|
+
check: "projection-unreachable",
|
|
1310
1718
|
severity: "unknown",
|
|
1311
|
-
label: "
|
|
1312
|
-
subject: `could not list secrets in the ${
|
|
1719
|
+
label: "Destination",
|
|
1720
|
+
subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
|
|
1313
1721
|
detail: errorDetail(error)
|
|
1314
1722
|
}
|
|
1315
1723
|
];
|
|
@@ -1347,7 +1755,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1347
1755
|
}
|
|
1348
1756
|
if (!destOf(item.scope).has(key)) {
|
|
1349
1757
|
nameDrift.push({
|
|
1350
|
-
check: "
|
|
1758
|
+
check: "projection-name-drift",
|
|
1351
1759
|
severity: "warning",
|
|
1352
1760
|
label: "Declared, not pushed",
|
|
1353
1761
|
subject: item.variable,
|
|
@@ -1359,7 +1767,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1359
1767
|
for (const secret of repoSecrets) {
|
|
1360
1768
|
if (!allVariables.has(upper(secret.name))) {
|
|
1361
1769
|
nameDrift.push({
|
|
1362
|
-
check: "
|
|
1770
|
+
check: "projection-name-drift",
|
|
1363
1771
|
severity: "warning",
|
|
1364
1772
|
label: "In destination, not declared",
|
|
1365
1773
|
subject: secret.name,
|
|
@@ -1370,7 +1778,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1370
1778
|
for (const secret of envSecrets) {
|
|
1371
1779
|
if (!expectedEnv.has(upper(secret.name))) {
|
|
1372
1780
|
nameDrift.push({
|
|
1373
|
-
check: "
|
|
1781
|
+
check: "projection-name-drift",
|
|
1374
1782
|
severity: "warning",
|
|
1375
1783
|
label: "In destination, not declared",
|
|
1376
1784
|
subject: secret.name,
|
|
@@ -1394,7 +1802,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1394
1802
|
continue;
|
|
1395
1803
|
}
|
|
1396
1804
|
manualEdits.push({
|
|
1397
|
-
check: "
|
|
1805
|
+
check: "projection-manual-edit",
|
|
1398
1806
|
severity: "warning",
|
|
1399
1807
|
label: "Edited outside penv",
|
|
1400
1808
|
subject: item.variable,
|
|
@@ -1405,9 +1813,9 @@ async function sinkFindings(project, environment, override) {
|
|
|
1405
1813
|
findings.push(
|
|
1406
1814
|
...nameDrift.length > 0 ? nameDrift : [
|
|
1407
1815
|
{
|
|
1408
|
-
check: "
|
|
1816
|
+
check: "projection-name-drift",
|
|
1409
1817
|
severity: "pass",
|
|
1410
|
-
label: "
|
|
1818
|
+
label: "Destination names",
|
|
1411
1819
|
subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
|
|
1412
1820
|
}
|
|
1413
1821
|
]
|
|
@@ -1415,17 +1823,17 @@ async function sinkFindings(project, environment, override) {
|
|
|
1415
1823
|
findings.push(
|
|
1416
1824
|
...manualEdits.length > 0 ? manualEdits : [
|
|
1417
1825
|
{
|
|
1418
|
-
check: "
|
|
1826
|
+
check: "projection-manual-edit",
|
|
1419
1827
|
severity: "pass",
|
|
1420
|
-
label: "
|
|
1828
|
+
label: "Destination hand-edits",
|
|
1421
1829
|
subject: `no secret has changed outside penv since its last push for ${environment}`
|
|
1422
1830
|
}
|
|
1423
1831
|
]
|
|
1424
1832
|
);
|
|
1425
1833
|
findings.push({
|
|
1426
|
-
check: "
|
|
1834
|
+
check: "projection-value-drift",
|
|
1427
1835
|
severity: "unknown",
|
|
1428
|
-
label: "
|
|
1836
|
+
label: "Destination values",
|
|
1429
1837
|
subject: "cannot be read back from a write-only destination",
|
|
1430
1838
|
detail: "value drift between the tree and the destination is unknowable by design"
|
|
1431
1839
|
});
|
|
@@ -1454,6 +1862,9 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1454
1862
|
return { kind: "read", subjects: local };
|
|
1455
1863
|
}
|
|
1456
1864
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
1865
|
+
if (!holdsRecords(source)) {
|
|
1866
|
+
return { kind: "read", subjects: local };
|
|
1867
|
+
}
|
|
1457
1868
|
try {
|
|
1458
1869
|
const subjects = await Promise.all(
|
|
1459
1870
|
local.map(async ({ resolution }) => ({
|
|
@@ -1602,6 +2013,9 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1602
2013
|
];
|
|
1603
2014
|
}
|
|
1604
2015
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
2016
|
+
if (!holdsRecords(source)) {
|
|
2017
|
+
return [];
|
|
2018
|
+
}
|
|
1605
2019
|
let local;
|
|
1606
2020
|
let remote;
|
|
1607
2021
|
try {
|
|
@@ -1671,28 +2085,57 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1671
2085
|
}
|
|
1672
2086
|
];
|
|
1673
2087
|
}
|
|
2088
|
+
var GLYPHS = {
|
|
2089
|
+
pass: CHECK,
|
|
2090
|
+
warning: WARN,
|
|
2091
|
+
failure: CROSS,
|
|
2092
|
+
unknown: UNKNOWN
|
|
2093
|
+
};
|
|
2094
|
+
function summarize(report) {
|
|
2095
|
+
const counts = { pass: 0, warning: 0, failure: 0, unknown: 0 };
|
|
2096
|
+
for (const finding of report.findings) {
|
|
2097
|
+
counts[finding.severity] += 1;
|
|
2098
|
+
}
|
|
2099
|
+
const parts = [out.green(`${counts.pass} passed`)];
|
|
2100
|
+
if (counts.warning > 0) {
|
|
2101
|
+
parts.push(out.yellow(`${counts.warning} ${counts.warning === 1 ? "warning" : "warnings"}`));
|
|
2102
|
+
}
|
|
2103
|
+
if (counts.failure > 0) {
|
|
2104
|
+
parts.push(out.red(`${counts.failure} ${counts.failure === 1 ? "failure" : "failures"}`));
|
|
2105
|
+
}
|
|
2106
|
+
if (counts.unknown > 0) {
|
|
2107
|
+
parts.push(out.dim(`${counts.unknown} could not be checked`));
|
|
2108
|
+
}
|
|
2109
|
+
const verdict = report.ok ? out.green(CHECK) : out.red(CROSS);
|
|
2110
|
+
return `${verdict} ${report.findings.length} checks: ${parts.join(out.dim(" \xB7 "))}`;
|
|
2111
|
+
}
|
|
1674
2112
|
function renderDoctor(report) {
|
|
1675
2113
|
const rows = report.findings.map((finding) => ({
|
|
1676
|
-
glyph: finding.severity
|
|
2114
|
+
glyph: GLYPHS[finding.severity],
|
|
1677
2115
|
label: finding.label,
|
|
1678
2116
|
...finding.subject === void 0 ? {} : { subject: finding.subject },
|
|
1679
2117
|
...finding.detail === void 0 ? {} : { detail: finding.detail }
|
|
1680
2118
|
}));
|
|
1681
|
-
const lines =
|
|
2119
|
+
const lines = [heading("penv doctor", `environment ${report.environment}`), ""];
|
|
2120
|
+
lines.push(...formatRows(rows));
|
|
1682
2121
|
const remedies = [
|
|
1683
2122
|
...new Set(
|
|
1684
2123
|
report.findings.filter((finding) => finding.severity !== "pass").map((finding) => finding.remedy).filter((remedy) => remedy !== void 0)
|
|
1685
2124
|
)
|
|
1686
2125
|
];
|
|
1687
|
-
|
|
1688
|
-
lines.push(
|
|
2126
|
+
if (remedies.length > 0) {
|
|
2127
|
+
lines.push("");
|
|
2128
|
+
for (const remedy of remedies) {
|
|
2129
|
+
lines.push(tip(remedy));
|
|
2130
|
+
}
|
|
1689
2131
|
}
|
|
2132
|
+
lines.push("", summarize(report));
|
|
1690
2133
|
return lines;
|
|
1691
2134
|
}
|
|
1692
2135
|
var doctorCommand = defineCommand3({
|
|
1693
2136
|
meta: {
|
|
1694
2137
|
name: "doctor",
|
|
1695
|
-
description: "Report missing, weak, unused, fallback, plaintext-secret, and
|
|
2138
|
+
description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
|
|
1696
2139
|
},
|
|
1697
2140
|
args: {
|
|
1698
2141
|
env: { type: "string", description: "The environment to report on" }
|
|
@@ -1701,7 +2144,8 @@ var doctorCommand = defineCommand3({
|
|
|
1701
2144
|
return guard(async () => {
|
|
1702
2145
|
const report = await runDoctor({
|
|
1703
2146
|
cwd: process.cwd(),
|
|
1704
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
2147
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2148
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1705
2149
|
});
|
|
1706
2150
|
write(renderDoctor(report));
|
|
1707
2151
|
if (!report.ok) {
|
|
@@ -1716,14 +2160,14 @@ import {
|
|
|
1716
2160
|
formatValueFile as formatValueFile4,
|
|
1717
2161
|
isSecret as isSecret3,
|
|
1718
2162
|
openValue as openValue3,
|
|
1719
|
-
PenvError as
|
|
2163
|
+
PenvError as PenvError8,
|
|
1720
2164
|
parameterId as parameterId4,
|
|
1721
2165
|
sealValue as sealValue2
|
|
1722
2166
|
} from "@penvhq/core";
|
|
1723
2167
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1724
2168
|
|
|
1725
2169
|
// src/commands/set.ts
|
|
1726
|
-
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";
|
|
1727
2171
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1728
2172
|
function scopeFrom(options) {
|
|
1729
2173
|
if (options.local === true) {
|
|
@@ -1743,7 +2187,7 @@ function targetScope(project, options, key) {
|
|
|
1743
2187
|
return scopeFrom(options);
|
|
1744
2188
|
}
|
|
1745
2189
|
if (environment.trim().length === 0) {
|
|
1746
|
-
throw new
|
|
2190
|
+
throw new PenvError7(
|
|
1747
2191
|
"ENVIRONMENT_FLAG_EMPTY",
|
|
1748
2192
|
`\`--env\` for parameter ${key} names no environment`,
|
|
1749
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.`
|
|
@@ -1756,7 +2200,7 @@ function policyEnvironment(project, options) {
|
|
|
1756
2200
|
}
|
|
1757
2201
|
function sealFor(project, file, value, parameter, environment) {
|
|
1758
2202
|
if (environment === void 0) {
|
|
1759
|
-
throw new
|
|
2203
|
+
throw new PenvError7(
|
|
1760
2204
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1761
2205
|
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
|
|
1762
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."
|
|
@@ -1858,7 +2302,7 @@ function twins(project, key, options) {
|
|
|
1858
2302
|
}
|
|
1859
2303
|
function environmentFor(project, options, verb) {
|
|
1860
2304
|
if (options.environment === void 0) {
|
|
1861
|
-
throw new
|
|
2305
|
+
throw new PenvError8(
|
|
1862
2306
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1863
2307
|
`\`penv ${verb}\` names no environment, and keys are declared per environment`,
|
|
1864
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."
|
|
@@ -1877,7 +2321,7 @@ async function runEncrypt(options) {
|
|
|
1877
2321
|
const value = await readOne(project, plain);
|
|
1878
2322
|
if (value === void 0) {
|
|
1879
2323
|
const already = await readOne(project, sealed);
|
|
1880
|
-
throw new
|
|
2324
|
+
throw new PenvError8(
|
|
1881
2325
|
"PARAMETER_ABSENT",
|
|
1882
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)}`,
|
|
1883
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."
|
|
@@ -1898,7 +2342,7 @@ async function runDecrypt(options) {
|
|
|
1898
2342
|
const [plain, sealed] = twins(project, options.key, options);
|
|
1899
2343
|
const parameter = parameterId4(plain);
|
|
1900
2344
|
if (isSecret3(await project.provider.readMeta(plain), environment)) {
|
|
1901
|
-
throw new
|
|
2345
|
+
throw new PenvError8(
|
|
1902
2346
|
"SECRET_DECRYPT_REFUSED",
|
|
1903
2347
|
`Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
|
|
1904
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."
|
|
@@ -1906,7 +2350,7 @@ async function runDecrypt(options) {
|
|
|
1906
2350
|
}
|
|
1907
2351
|
const stored = await readOne(project, sealed);
|
|
1908
2352
|
if (stored === void 0) {
|
|
1909
|
-
throw new
|
|
2353
|
+
throw new PenvError8(
|
|
1910
2354
|
"PARAMETER_ABSENT",
|
|
1911
2355
|
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
1912
2356
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
@@ -1929,7 +2373,7 @@ async function runDecrypt(options) {
|
|
|
1929
2373
|
removed: formatValueFile4(sealed)
|
|
1930
2374
|
};
|
|
1931
2375
|
}
|
|
1932
|
-
var UndecryptableAt = class extends
|
|
2376
|
+
var UndecryptableAt = class extends PenvError8 {
|
|
1933
2377
|
constructor(parameter, environment, location, detail) {
|
|
1934
2378
|
super(
|
|
1935
2379
|
"VALUE_UNDECRYPTABLE",
|
|
@@ -1984,14 +2428,14 @@ var decryptCommand = defineCommand5({
|
|
|
1984
2428
|
});
|
|
1985
2429
|
|
|
1986
2430
|
// src/commands/fill.ts
|
|
1987
|
-
import {
|
|
1988
|
-
import { PenvError as PenvError8 } from "@penvhq/core";
|
|
2431
|
+
import { PenvError as PenvError9 } from "@penvhq/core";
|
|
1989
2432
|
import { defineCommand as defineCommand6 } from "citty";
|
|
1990
2433
|
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
1991
2434
|
async function runFill(options) {
|
|
1992
2435
|
const validation = await runValidate({
|
|
1993
2436
|
cwd: options.cwd,
|
|
1994
|
-
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
2437
|
+
...options.environment === void 0 ? {} : { environment: options.environment },
|
|
2438
|
+
...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
|
|
1995
2439
|
});
|
|
1996
2440
|
const environment = validation.environment;
|
|
1997
2441
|
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
@@ -1999,7 +2443,7 @@ async function runFill(options) {
|
|
|
1999
2443
|
const detail = blockers.map(
|
|
2000
2444
|
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
2001
2445
|
).join("\n");
|
|
2002
|
-
throw new
|
|
2446
|
+
throw new PenvError9(
|
|
2003
2447
|
"FILL_BLOCKED",
|
|
2004
2448
|
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
2005
2449
|
${detail}`,
|
|
@@ -2008,6 +2452,7 @@ ${detail}`,
|
|
|
2008
2452
|
}
|
|
2009
2453
|
const written = [];
|
|
2010
2454
|
const skipped2 = [];
|
|
2455
|
+
const kept = [];
|
|
2011
2456
|
const unreachable = [];
|
|
2012
2457
|
for (const drift of validation.drift.declared) {
|
|
2013
2458
|
if (drift.ref === void 0) {
|
|
@@ -2016,7 +2461,12 @@ ${detail}`,
|
|
|
2016
2461
|
}
|
|
2017
2462
|
const ref = drift.ref;
|
|
2018
2463
|
const key = [...ref.namespace, ref.name].join("/");
|
|
2019
|
-
const value = await options.ask({
|
|
2464
|
+
const value = await options.ask({
|
|
2465
|
+
parameter: key,
|
|
2466
|
+
environment,
|
|
2467
|
+
secret: false,
|
|
2468
|
+
optional: false
|
|
2469
|
+
});
|
|
2020
2470
|
if (value === void 0 || value === "") {
|
|
2021
2471
|
skipped2.push(drift.subject);
|
|
2022
2472
|
continue;
|
|
@@ -2028,17 +2478,45 @@ ${detail}`,
|
|
|
2028
2478
|
encrypted: result2.encrypted
|
|
2029
2479
|
});
|
|
2030
2480
|
}
|
|
2031
|
-
|
|
2481
|
+
for (const item of validation.drift.optional) {
|
|
2482
|
+
if (item.ref === void 0) {
|
|
2483
|
+
unreachable.push({ subject: item.subject, remedy: item.remedy });
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
const ref = item.ref;
|
|
2487
|
+
const key = [...ref.namespace, ref.name].join("/");
|
|
2488
|
+
const value = await options.ask({
|
|
2489
|
+
parameter: key,
|
|
2490
|
+
environment,
|
|
2491
|
+
secret: false,
|
|
2492
|
+
optional: true,
|
|
2493
|
+
...item.defaultValue === void 0 ? {} : { defaultValue: item.defaultValue }
|
|
2494
|
+
});
|
|
2495
|
+
if (value === void 0 || value === "") {
|
|
2496
|
+
kept.push(item.subject);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const result2 = await runSet({ cwd: options.cwd, key, value, environment });
|
|
2500
|
+
written.push({
|
|
2501
|
+
parameter: item.subject,
|
|
2502
|
+
location: result2.location,
|
|
2503
|
+
encrypted: result2.encrypted
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
return { environment, written, skipped: skipped2, kept, unreachable };
|
|
2032
2507
|
}
|
|
2033
2508
|
function summaryLine(result2) {
|
|
2034
2509
|
const filled = result2.written.length;
|
|
2035
|
-
if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
|
|
2036
|
-
return
|
|
2510
|
+
if (filled === 0 && result2.skipped.length === 0 && result2.kept.length === 0 && result2.unreachable.length === 0) {
|
|
2511
|
+
return `${out.green(CHECK)} Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
|
|
2037
2512
|
}
|
|
2038
2513
|
const parts = [`${filled} written`];
|
|
2039
2514
|
if (result2.skipped.length > 0) {
|
|
2040
2515
|
parts.push(`${result2.skipped.length} skipped`);
|
|
2041
2516
|
}
|
|
2517
|
+
if (result2.kept.length > 0) {
|
|
2518
|
+
parts.push(`${result2.kept.length} left to the schema's defaults`);
|
|
2519
|
+
}
|
|
2042
2520
|
if (result2.unreachable.length > 0) {
|
|
2043
2521
|
parts.push(`${result2.unreachable.length} unreachable`);
|
|
2044
2522
|
}
|
|
@@ -2059,6 +2537,16 @@ function renderFill(result2) {
|
|
|
2059
2537
|
lines.push(summaryLine(result2));
|
|
2060
2538
|
return lines;
|
|
2061
2539
|
}
|
|
2540
|
+
function contextFor(prompt2) {
|
|
2541
|
+
if (!prompt2.optional) {
|
|
2542
|
+
return `${prompt2.environment}, Enter to skip`;
|
|
2543
|
+
}
|
|
2544
|
+
if (prompt2.defaultValue === void 0) {
|
|
2545
|
+
return `${prompt2.environment} \xB7 optional, Enter leaves it unset`;
|
|
2546
|
+
}
|
|
2547
|
+
const shown = prompt2.defaultValue.length > 24 ? `${prompt2.defaultValue.slice(0, 23)}\u2026` : prompt2.defaultValue;
|
|
2548
|
+
return `${prompt2.environment} \xB7 optional, Enter keeps ${shown}`;
|
|
2549
|
+
}
|
|
2062
2550
|
var fillCommand = defineCommand6({
|
|
2063
2551
|
meta: {
|
|
2064
2552
|
name: "fill",
|
|
@@ -2069,20 +2557,21 @@ var fillCommand = defineCommand6({
|
|
|
2069
2557
|
},
|
|
2070
2558
|
run({ args }) {
|
|
2071
2559
|
return guard(async () => {
|
|
2072
|
-
const
|
|
2073
|
-
const ask = (
|
|
2560
|
+
const reader = lineReader();
|
|
2561
|
+
const ask = (prompt2) => reader.ask(prompt(prompt2.parameter, contextFor(prompt2)));
|
|
2074
2562
|
try {
|
|
2075
2563
|
write(
|
|
2076
2564
|
renderFill(
|
|
2077
2565
|
await runFill({
|
|
2078
2566
|
cwd: process.cwd(),
|
|
2079
2567
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2080
|
-
ask
|
|
2568
|
+
ask,
|
|
2569
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
2081
2570
|
})
|
|
2082
2571
|
)
|
|
2083
2572
|
);
|
|
2084
2573
|
} finally {
|
|
2085
|
-
|
|
2574
|
+
reader.close();
|
|
2086
2575
|
}
|
|
2087
2576
|
});
|
|
2088
2577
|
}
|
|
@@ -2094,7 +2583,7 @@ import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
|
2094
2583
|
import {
|
|
2095
2584
|
checkNameCollisions as checkNameCollisions3,
|
|
2096
2585
|
effectiveMeta as effectiveMeta2,
|
|
2097
|
-
PenvError as
|
|
2586
|
+
PenvError as PenvError10,
|
|
2098
2587
|
requireValue as requireValue2,
|
|
2099
2588
|
serializeDotenv,
|
|
2100
2589
|
variableName as variableName4
|
|
@@ -2118,7 +2607,7 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2118
2607
|
const winner = resolution.winner;
|
|
2119
2608
|
if (winner?.file.encrypted === true) {
|
|
2120
2609
|
if (!allowDecrypt) {
|
|
2121
|
-
throw new
|
|
2610
|
+
throw new PenvError10(
|
|
2122
2611
|
"ENCRYPTED_VALUE_REFUSED",
|
|
2123
2612
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
2124
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.`
|
|
@@ -2142,12 +2631,12 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2142
2631
|
}
|
|
2143
2632
|
function generateDotenv(options) {
|
|
2144
2633
|
const project = openProject(options.cwd);
|
|
2145
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2634
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2146
2635
|
return serializeDotenv(entriesFor(project, environment, options.allowDecrypt === true).entries);
|
|
2147
2636
|
}
|
|
2148
2637
|
function runGenerate(options) {
|
|
2149
2638
|
const project = openProject(options.cwd);
|
|
2150
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2639
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2151
2640
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
2152
2641
|
const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
|
|
2153
2642
|
writeFileSync(file, serializeDotenv(entries), "utf8");
|
|
@@ -2193,7 +2682,8 @@ var generateCommand = defineCommand7({
|
|
|
2193
2682
|
cwd,
|
|
2194
2683
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2195
2684
|
...args.out === void 0 ? {} : { out: args.out },
|
|
2196
|
-
...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"])
|
|
2197
2687
|
});
|
|
2198
2688
|
write(renderGenerate(result2, cwd));
|
|
2199
2689
|
});
|
|
@@ -2201,7 +2691,7 @@ var generateCommand = defineCommand7({
|
|
|
2201
2691
|
});
|
|
2202
2692
|
|
|
2203
2693
|
// src/commands/get.ts
|
|
2204
|
-
import { PenvError as
|
|
2694
|
+
import { PenvError as PenvError11, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
|
|
2205
2695
|
import { defineCommand as defineCommand8 } from "citty";
|
|
2206
2696
|
async function runGet(options) {
|
|
2207
2697
|
const project = openProject(options.cwd);
|
|
@@ -2211,7 +2701,7 @@ async function runGet(options) {
|
|
|
2211
2701
|
const resolution = await resolveParameter(ref, environment, project.provider, keys);
|
|
2212
2702
|
const value = requireValue3(resolution, environment);
|
|
2213
2703
|
if (value === void 0) {
|
|
2214
|
-
throw new
|
|
2704
|
+
throw new PenvError11(
|
|
2215
2705
|
"PARAMETER_ABSENT",
|
|
2216
2706
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
2217
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.`
|
|
@@ -2251,12 +2741,14 @@ async function runExplain(options) {
|
|
|
2251
2741
|
function renderExplain(explanation) {
|
|
2252
2742
|
const target = explanation.location === void 0 ? "nothing" : `${PENV_DIR}/${explanation.location}`;
|
|
2253
2743
|
const rows = explanation.candidates.map((candidate) => [
|
|
2254
|
-
candidate.location,
|
|
2255
|
-
candidate.wins ? "present, wins" :
|
|
2744
|
+
candidate.wins ? candidate.location : out.dim(candidate.location),
|
|
2745
|
+
candidate.wins ? out.green("present, wins") : out.dim(
|
|
2746
|
+
candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
|
|
2747
|
+
)
|
|
2256
2748
|
]);
|
|
2257
2749
|
return [
|
|
2258
|
-
`${explanation.parameter} resolves to ${target} for environment ${explanation.environment}`,
|
|
2259
|
-
...explanation.undecryptable === void 0 ? [] : [` penv cannot decrypt it: ${explanation.undecryptable}`],
|
|
2750
|
+
`${out.bold(explanation.parameter)} resolves to ${target} for environment ${explanation.environment}`,
|
|
2751
|
+
...explanation.undecryptable === void 0 ? [] : [` ${out.red(CROSS)} penv cannot decrypt it: ${explanation.undecryptable}`],
|
|
2260
2752
|
"",
|
|
2261
2753
|
...columns(rows).map((line) => ` ${line}`)
|
|
2262
2754
|
];
|
|
@@ -2296,7 +2788,7 @@ import {
|
|
|
2296
2788
|
isReservedToken as isReservedToken3,
|
|
2297
2789
|
loadConfigFrom as loadConfigFrom2,
|
|
2298
2790
|
lookupEnvironment,
|
|
2299
|
-
PenvError as
|
|
2791
|
+
PenvError as PenvError13,
|
|
2300
2792
|
parseDotenv,
|
|
2301
2793
|
ReservedTokenError as ReservedTokenError3,
|
|
2302
2794
|
refFromVariable,
|
|
@@ -2416,7 +2908,7 @@ import {
|
|
|
2416
2908
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
2417
2909
|
isLegalEnvironmentName,
|
|
2418
2910
|
loadConfigFrom,
|
|
2419
|
-
PenvError as
|
|
2911
|
+
PenvError as PenvError12,
|
|
2420
2912
|
RESERVED_TOKENS,
|
|
2421
2913
|
schemaFileOf as schemaFileOf2,
|
|
2422
2914
|
schemaInsideTree,
|
|
@@ -2460,7 +2952,7 @@ function suggestEnvironments(root) {
|
|
|
2460
2952
|
return [...found].sort();
|
|
2461
2953
|
}
|
|
2462
2954
|
function emptyFlag(flag) {
|
|
2463
|
-
return new
|
|
2955
|
+
return new PenvError12(
|
|
2464
2956
|
"INIT_FLAG_EMPTY",
|
|
2465
2957
|
`\`--${flag}\` was given without a value`,
|
|
2466
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."
|
|
@@ -2523,7 +3015,7 @@ function planInit(root, flags = {}) {
|
|
|
2523
3015
|
throw emptyFlag("alias");
|
|
2524
3016
|
}
|
|
2525
3017
|
if (!ALIAS_NAME.test(alias)) {
|
|
2526
|
-
throw new
|
|
3018
|
+
throw new PenvError12(
|
|
2527
3019
|
"INIT_ALIAS_INVALID",
|
|
2528
3020
|
`\`${alias}\` is not an alias penv can write`,
|
|
2529
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.`
|
|
@@ -2561,23 +3053,27 @@ function planInit(root, flags = {}) {
|
|
|
2561
3053
|
function renderPlan(plan2) {
|
|
2562
3054
|
const rows = [];
|
|
2563
3055
|
rows.push([
|
|
2564
|
-
"
|
|
2565
|
-
plan2.suggestedEnvironments.length === 0 ? "" : `[${plan2.suggestedEnvironments.join(", ")}]
|
|
2566
|
-
|
|
3056
|
+
` ${out.dim("environments")}`,
|
|
3057
|
+
plan2.suggestedEnvironments.length === 0 ? "" : out.cyan(`[${plan2.suggestedEnvironments.join(", ")}]`),
|
|
3058
|
+
out.dim(
|
|
3059
|
+
plan2.suggestedEnvironments.length === 0 ? "\u2190 name them, or Enter to leave the whitelist empty" : "\u2190 from your .env files; edit, or Enter to accept"
|
|
3060
|
+
)
|
|
2567
3061
|
]);
|
|
2568
3062
|
rows.push([
|
|
2569
|
-
"
|
|
3063
|
+
` ${out.dim("schemaFile")}`,
|
|
2570
3064
|
plan2.decisions.schemaFile,
|
|
2571
|
-
plan2.decisions.schemaFile === DEFAULT_SCHEMA_FILE2 ? "" : `(default: ${DEFAULT_SCHEMA_FILE2})`
|
|
3065
|
+
plan2.decisions.schemaFile === DEFAULT_SCHEMA_FILE2 ? "" : out.dim(`(default: ${DEFAULT_SCHEMA_FILE2})`)
|
|
2572
3066
|
]);
|
|
2573
3067
|
for (const prefix of plan2.decisions.publicPrefixes) {
|
|
2574
|
-
rows.push(["
|
|
3068
|
+
rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
|
|
2575
3069
|
}
|
|
2576
|
-
const headline =
|
|
3070
|
+
const headline = out.bold(
|
|
3071
|
+
plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`
|
|
3072
|
+
);
|
|
2577
3073
|
return [headline, "", ...columns(rows), ""];
|
|
2578
3074
|
}
|
|
2579
3075
|
function environmentsHint(plan2) {
|
|
2580
|
-
return plan2.suggestedEnvironments.length === 0 ? "environments
|
|
3076
|
+
return plan2.suggestedEnvironments.length === 0 ? prompt("environments", "comma-separated, Enter for none") : prompt("environments", 'Enter to accept, "none" for an empty whitelist');
|
|
2581
3077
|
}
|
|
2582
3078
|
async function promptForDecisions(plan2, io) {
|
|
2583
3079
|
for (const line of renderPlan(plan2)) {
|
|
@@ -2592,7 +3088,7 @@ async function promptForDecisions(plan2, io) {
|
|
|
2592
3088
|
);
|
|
2593
3089
|
io.write("");
|
|
2594
3090
|
}
|
|
2595
|
-
const proceed = (await io.ask("Proceed?
|
|
3091
|
+
const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
|
|
2596
3092
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
2597
3093
|
return void 0;
|
|
2598
3094
|
}
|
|
@@ -2615,6 +3111,15 @@ ${body}
|
|
|
2615
3111
|
// code. Importing it loads configuration and throws (naming the parameter and
|
|
2616
3112
|
// environment) if anything required is missing or invalid.
|
|
2617
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
|
+
}
|
|
2618
3123
|
`;
|
|
2619
3124
|
}
|
|
2620
3125
|
function renderEnvironments(decisions) {
|
|
@@ -2624,14 +3129,14 @@ function renderEnvironments(decisions) {
|
|
|
2624
3129
|
// can read off your codebase, and an environment you do not have is worse
|
|
2625
3130
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
2626
3131
|
// environments: ["development", "production"],
|
|
2627
|
-
// providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
|
|
3132
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
|
|
2628
3133
|
environments: [],
|
|
2629
3134
|
|
|
2630
3135
|
providers: {},
|
|
2631
3136
|
`;
|
|
2632
3137
|
}
|
|
2633
3138
|
const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
|
|
2634
|
-
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" },
|
|
2635
3140
|
`).join("");
|
|
2636
3141
|
return `${shared} environments: [${names}],
|
|
2637
3142
|
|
|
@@ -2810,7 +3315,7 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
2810
3315
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
2811
3316
|
}
|
|
2812
3317
|
function shapeError(what, target, alias) {
|
|
2813
|
-
return new
|
|
3318
|
+
return new PenvError12(
|
|
2814
3319
|
"TSCONFIG_SHAPE",
|
|
2815
3320
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
2816
3321
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
@@ -2997,7 +3502,8 @@ function renderInit(result2) {
|
|
|
2997
3502
|
return [
|
|
2998
3503
|
...formatSteps(steps),
|
|
2999
3504
|
"",
|
|
3000
|
-
|
|
3505
|
+
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${result2.decisions.schemaFile}, then:`,
|
|
3506
|
+
tip(out.cyan("penv set <key>")),
|
|
3001
3507
|
...result2.decisions.environments.length === 0 ? [
|
|
3002
3508
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
3003
3509
|
] : []
|
|
@@ -3089,7 +3595,7 @@ function assertImportable(ref, variable) {
|
|
|
3089
3595
|
if (!ref.name.includes(".")) {
|
|
3090
3596
|
return;
|
|
3091
3597
|
}
|
|
3092
|
-
throw new
|
|
3598
|
+
throw new PenvError13(
|
|
3093
3599
|
"IMPORT_UNPARSEABLE_NAME",
|
|
3094
3600
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
3095
3601
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
@@ -3108,10 +3614,10 @@ function assertRoundTrips(ref, variable, config) {
|
|
|
3108
3614
|
return;
|
|
3109
3615
|
}
|
|
3110
3616
|
const generated = variableName5(ref, config);
|
|
3111
|
-
throw new
|
|
3617
|
+
throw new PenvError13(
|
|
3112
3618
|
"IMPORT_LOSSY_NAME",
|
|
3113
3619
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
3114
|
-
`\`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.`
|
|
3115
3621
|
);
|
|
3116
3622
|
}
|
|
3117
3623
|
function collisionsIn(refs, config) {
|
|
@@ -3192,7 +3698,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
3192
3698
|
if (value.length > 0) {
|
|
3193
3699
|
return value;
|
|
3194
3700
|
}
|
|
3195
|
-
throw new
|
|
3701
|
+
throw new PenvError13(
|
|
3196
3702
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
3197
3703
|
`\`--env\` for the import of ${source} names no environment`,
|
|
3198
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.`
|
|
@@ -3202,7 +3708,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
3202
3708
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
3203
3709
|
return;
|
|
3204
3710
|
}
|
|
3205
|
-
throw new
|
|
3711
|
+
throw new PenvError13(
|
|
3206
3712
|
"IMPORT_ENV_CONFLICT",
|
|
3207
3713
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
3208
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.`
|
|
@@ -3246,7 +3752,7 @@ function importDotenv(options) {
|
|
|
3246
3752
|
const cwd = resolve5(options.cwd);
|
|
3247
3753
|
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
3248
3754
|
if (!existsSync3(file)) {
|
|
3249
|
-
throw new
|
|
3755
|
+
throw new PenvError13(
|
|
3250
3756
|
"IMPORT_FILE_MISSING",
|
|
3251
3757
|
`There is no file at ${file} to import`,
|
|
3252
3758
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
@@ -3391,7 +3897,7 @@ var importCommand = defineCommand10({
|
|
|
3391
3897
|
|
|
3392
3898
|
// src/commands/key.ts
|
|
3393
3899
|
import { randomBytes } from "crypto";
|
|
3394
|
-
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as
|
|
3900
|
+
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError14 } from "@penvhq/core";
|
|
3395
3901
|
import { defineCommand as defineCommand11 } from "citty";
|
|
3396
3902
|
|
|
3397
3903
|
// src/keychain.ts
|
|
@@ -3424,7 +3930,7 @@ function runKeyCreate(options) {
|
|
|
3424
3930
|
const environment = targetEnvironment(project, options.environment);
|
|
3425
3931
|
const declared = project.config.keys?.[environment];
|
|
3426
3932
|
if (declared === void 0) {
|
|
3427
|
-
throw new
|
|
3933
|
+
throw new PenvError14(
|
|
3428
3934
|
"KEY_SOURCE_UNDECLARED",
|
|
3429
3935
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
3430
3936
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
@@ -3438,14 +3944,14 @@ function runKeyCreate(options) {
|
|
|
3438
3944
|
try {
|
|
3439
3945
|
existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
|
|
3440
3946
|
} catch (cause) {
|
|
3441
|
-
throw new
|
|
3947
|
+
throw new PenvError14(
|
|
3442
3948
|
"KEYCHAIN_UNAVAILABLE",
|
|
3443
3949
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
3444
3950
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
3445
3951
|
);
|
|
3446
3952
|
}
|
|
3447
3953
|
if (existing !== null) {
|
|
3448
|
-
throw new
|
|
3954
|
+
throw new PenvError14(
|
|
3449
3955
|
"KEY_EXISTS",
|
|
3450
3956
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
3451
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."
|
|
@@ -3466,20 +3972,28 @@ function runKeyCreate(options) {
|
|
|
3466
3972
|
function renderKeyCreate(result2) {
|
|
3467
3973
|
if (result2.source === "keychain") {
|
|
3468
3974
|
return [
|
|
3469
|
-
|
|
3975
|
+
`${out.green(CHECK)} A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
3470
3976
|
"",
|
|
3471
|
-
|
|
3472
|
-
|
|
3977
|
+
out.dim(
|
|
3978
|
+
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running"
|
|
3979
|
+
),
|
|
3980
|
+
out.dim(
|
|
3981
|
+
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
3982
|
+
)
|
|
3473
3983
|
];
|
|
3474
3984
|
}
|
|
3475
3985
|
return [
|
|
3476
|
-
|
|
3986
|
+
`${out.green(CHECK)} A new key for environment ${result2.environment}. penv did not store it.`,
|
|
3477
3987
|
"",
|
|
3478
|
-
` ${result2.variable}=${result2.key}`,
|
|
3988
|
+
` ${out.cyan(`${result2.variable}=${result2.key}`)}`,
|
|
3479
3989
|
"",
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3990
|
+
out.dim(
|
|
3991
|
+
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014"
|
|
3992
|
+
),
|
|
3993
|
+
out.dim(
|
|
3994
|
+
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable"
|
|
3995
|
+
),
|
|
3996
|
+
out.dim("without it, and penv keeps no copy to fall back on.")
|
|
3483
3997
|
];
|
|
3484
3998
|
}
|
|
3485
3999
|
var keyCommand = defineCommand11({
|
|
@@ -3547,11 +4061,31 @@ async function runList(options) {
|
|
|
3547
4061
|
}
|
|
3548
4062
|
return { environment, parameters };
|
|
3549
4063
|
}
|
|
4064
|
+
function paintScope(entry) {
|
|
4065
|
+
if (entry.scope === "absent" || entry.viaUnscopedFallback) {
|
|
4066
|
+
return out.yellow(entry.scope);
|
|
4067
|
+
}
|
|
4068
|
+
return out.green(entry.scope);
|
|
4069
|
+
}
|
|
3550
4070
|
function renderList(result2) {
|
|
3551
4071
|
if (result2.parameters.length === 0) {
|
|
3552
|
-
return [
|
|
4072
|
+
return [
|
|
4073
|
+
`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`,
|
|
4074
|
+
tip(`penv set <key> --env ${result2.environment}`)
|
|
4075
|
+
];
|
|
3553
4076
|
}
|
|
3554
|
-
|
|
4077
|
+
const header = ["parameter", "scope", "variable", ""].map((cell) => out.dim(cell));
|
|
4078
|
+
const rows = result2.parameters.map((entry) => [
|
|
4079
|
+
entry.parameter,
|
|
4080
|
+
paintScope(entry),
|
|
4081
|
+
entry.variable,
|
|
4082
|
+
entry.encrypted ? out.dim("encrypted") : ""
|
|
4083
|
+
]);
|
|
4084
|
+
return [
|
|
4085
|
+
heading("penv list", `environment ${result2.environment}`),
|
|
4086
|
+
"",
|
|
4087
|
+
...columns([header, ...rows])
|
|
4088
|
+
];
|
|
3555
4089
|
}
|
|
3556
4090
|
var listCommand = defineCommand12({
|
|
3557
4091
|
meta: { name: "list", description: "List parameters" },
|
|
@@ -3576,7 +4110,7 @@ import {
|
|
|
3576
4110
|
formatMetaFile,
|
|
3577
4111
|
formatValueFile as formatValueFile5,
|
|
3578
4112
|
openValue as openValue4,
|
|
3579
|
-
PenvError as
|
|
4113
|
+
PenvError as PenvError15,
|
|
3580
4114
|
parameterId as parameterId5,
|
|
3581
4115
|
sealValue as sealValue3
|
|
3582
4116
|
} from "@penvhq/core";
|
|
@@ -3595,7 +4129,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
3595
4129
|
}
|
|
3596
4130
|
const environment = environmentOf2(source);
|
|
3597
4131
|
if (environment === void 0) {
|
|
3598
|
-
throw new
|
|
4132
|
+
throw new PenvError15(
|
|
3599
4133
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3600
4134
|
`${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3601
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."
|
|
@@ -3604,7 +4138,7 @@ async function planFile(project, source, target, parameter) {
|
|
|
3604
4138
|
const keys = keySourceFor(project, environment);
|
|
3605
4139
|
const opened = openValue4(source, stored, keys);
|
|
3606
4140
|
if (opened.kind === "failed") {
|
|
3607
|
-
throw new
|
|
4141
|
+
throw new PenvError15(
|
|
3608
4142
|
"VALUE_UNDECRYPTABLE",
|
|
3609
4143
|
`${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3610
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."
|
|
@@ -3627,7 +4161,7 @@ async function runMove(options) {
|
|
|
3627
4161
|
assertWritableKey(options.to);
|
|
3628
4162
|
const to = refFromKey(options.to, project.config);
|
|
3629
4163
|
if (parameterId5(from) === parameterId5(to)) {
|
|
3630
|
-
throw new
|
|
4164
|
+
throw new PenvError15(
|
|
3631
4165
|
"PARAMETER_UNCHANGED",
|
|
3632
4166
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3633
4167
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3637,7 +4171,7 @@ async function runMove(options) {
|
|
|
3637
4171
|
const sources = filesOf(all, from);
|
|
3638
4172
|
const meta = await project.provider.readMeta(from);
|
|
3639
4173
|
if (sources.length === 0 && meta === void 0) {
|
|
3640
|
-
throw new
|
|
4174
|
+
throw new PenvError15(
|
|
3641
4175
|
"PARAMETER_ABSENT",
|
|
3642
4176
|
`Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
|
|
3643
4177
|
`\`penv list\` shows every parameter penv holds.`
|
|
@@ -3645,7 +4179,7 @@ async function runMove(options) {
|
|
|
3645
4179
|
}
|
|
3646
4180
|
const occupied2 = filesOf(all, to);
|
|
3647
4181
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3648
|
-
throw new
|
|
4182
|
+
throw new PenvError15(
|
|
3649
4183
|
"PARAMETER_EXISTS",
|
|
3650
4184
|
`Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
|
|
3651
4185
|
`Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
@@ -3699,8 +4233,9 @@ function renderMove(result2) {
|
|
|
3699
4233
|
const lines = formatRows(rows);
|
|
3700
4234
|
lines.push(
|
|
3701
4235
|
"",
|
|
3702
|
-
|
|
3703
|
-
|
|
4236
|
+
tip(
|
|
4237
|
+
`.penv/env.ts still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
|
|
4238
|
+
)
|
|
3704
4239
|
);
|
|
3705
4240
|
return lines;
|
|
3706
4241
|
}
|
|
@@ -3726,14 +4261,45 @@ var mvCommand = defineCommand13({
|
|
|
3726
4261
|
});
|
|
3727
4262
|
|
|
3728
4263
|
// src/commands/pull.ts
|
|
4264
|
+
import { holdsProjection as holdsProjection4, refFromVariable as refFromVariable2 } from "@penvhq/core";
|
|
3729
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
|
+
}
|
|
3730
4293
|
async function runPull(options) {
|
|
3731
4294
|
const project = openProject(options.cwd);
|
|
3732
|
-
const environment = targetEnvironment(project, options.environment);
|
|
3733
|
-
const source = await sourceProviderFor(project, environment);
|
|
4295
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
4296
|
+
const source = options.source ?? await sourceProviderFor(project, environment);
|
|
3734
4297
|
if (source.type === LOCAL_TREE_TYPE) {
|
|
3735
4298
|
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3736
4299
|
}
|
|
4300
|
+
if (holdsProjection4(source)) {
|
|
4301
|
+
return pullProjection(project, source, environment);
|
|
4302
|
+
}
|
|
3737
4303
|
const tree = localTree(project);
|
|
3738
4304
|
const files = await source.list();
|
|
3739
4305
|
let values = 0;
|
|
@@ -3768,6 +4334,22 @@ function renderPull(result2) {
|
|
|
3768
4334
|
}
|
|
3769
4335
|
]);
|
|
3770
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
|
+
}
|
|
3771
4353
|
return formatRows([
|
|
3772
4354
|
{
|
|
3773
4355
|
glyph: CHECK,
|
|
@@ -3789,13 +4371,17 @@ var pullCommand = defineCommand14({
|
|
|
3789
4371
|
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3790
4372
|
},
|
|
3791
4373
|
args: {
|
|
3792
|
-
env: {
|
|
4374
|
+
env: {
|
|
4375
|
+
type: "string",
|
|
4376
|
+
description: "The environment to pull (or pass it as a bare flag: --production)"
|
|
4377
|
+
}
|
|
3793
4378
|
},
|
|
3794
4379
|
run({ args }) {
|
|
3795
4380
|
return guard(async () => {
|
|
3796
4381
|
const result2 = await runPull({
|
|
3797
4382
|
cwd: process.cwd(),
|
|
3798
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
4383
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4384
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
3799
4385
|
});
|
|
3800
4386
|
write(renderPull(result2));
|
|
3801
4387
|
});
|
|
@@ -3878,7 +4464,8 @@ var removeCommand = defineCommand15({
|
|
|
3878
4464
|
import {
|
|
3879
4465
|
beginRotation,
|
|
3880
4466
|
completeRotation,
|
|
3881
|
-
|
|
4467
|
+
holdsRecords as holdsRecords2,
|
|
4468
|
+
PenvError as PenvError16,
|
|
3882
4469
|
retainsPrevious,
|
|
3883
4470
|
rotationOf as rotationOf2
|
|
3884
4471
|
} from "@penvhq/core";
|
|
@@ -3907,7 +4494,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
3907
4494
|
}
|
|
3908
4495
|
function requireNewValue(value, phase, key) {
|
|
3909
4496
|
if (value === void 0) {
|
|
3910
|
-
throw new
|
|
4497
|
+
throw new PenvError16(
|
|
3911
4498
|
"ROTATION_NO_VALUE",
|
|
3912
4499
|
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3913
4500
|
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
@@ -3917,7 +4504,7 @@ function requireNewValue(value, phase, key) {
|
|
|
3917
4504
|
}
|
|
3918
4505
|
function requireRetaining(provider, environment) {
|
|
3919
4506
|
if (!retainsPrevious(provider)) {
|
|
3920
|
-
throw new
|
|
4507
|
+
throw new PenvError16(
|
|
3921
4508
|
"ROTATION_NOT_RETAINING",
|
|
3922
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`,
|
|
3923
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."
|
|
@@ -3929,12 +4516,20 @@ async function runRotate(options) {
|
|
|
3929
4516
|
const project = openProject(options.cwd);
|
|
3930
4517
|
const environment = targetEnvironment(project, options.environment);
|
|
3931
4518
|
const ref = refFromKey(options.key, project.config);
|
|
3932
|
-
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;
|
|
3933
4528
|
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3934
4529
|
const before = await provider.readMeta(ref);
|
|
3935
4530
|
const { mechanism } = rotationOf2(before, environment);
|
|
3936
4531
|
if (mechanism === void 0) {
|
|
3937
|
-
throw new
|
|
4532
|
+
throw new PenvError16(
|
|
3938
4533
|
"ROTATION_NO_MECHANISM",
|
|
3939
4534
|
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3940
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.'
|
|
@@ -3944,7 +4539,7 @@ async function runRotate(options) {
|
|
|
3944
4539
|
const complete = options.complete === true;
|
|
3945
4540
|
if (mechanism === "atomic-cutover") {
|
|
3946
4541
|
if (begin || complete) {
|
|
3947
|
-
throw new
|
|
4542
|
+
throw new PenvError16(
|
|
3948
4543
|
"ROTATION_MECHANISM_MISMATCH",
|
|
3949
4544
|
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3950
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."
|
|
@@ -3958,7 +4553,7 @@ async function runRotate(options) {
|
|
|
3958
4553
|
}
|
|
3959
4554
|
const retaining = requireRetaining(provider, environment);
|
|
3960
4555
|
if (begin === complete) {
|
|
3961
|
-
throw new
|
|
4556
|
+
throw new PenvError16(
|
|
3962
4557
|
"ROTATION_PHASE_REQUIRED",
|
|
3963
4558
|
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3964
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."
|
|
@@ -4235,9 +4830,9 @@ function renderDrift(drift, environment) {
|
|
|
4235
4830
|
if (rows.length === 0) {
|
|
4236
4831
|
return [];
|
|
4237
4832
|
}
|
|
4238
|
-
const lines = ["", `Schema and tree differ for ${environment}
|
|
4833
|
+
const lines = ["", out.bold(`Schema and tree differ for ${environment}:`), ...formatRows(rows)];
|
|
4239
4834
|
for (const remedy of new Set(drift.declared.map((item) => item.remedy))) {
|
|
4240
|
-
lines.push(
|
|
4835
|
+
lines.push(tip(remedy));
|
|
4241
4836
|
}
|
|
4242
4837
|
return lines;
|
|
4243
4838
|
}
|
|
@@ -4268,7 +4863,7 @@ var watchCommand = defineCommand17({
|
|
|
4268
4863
|
process.exitCode = previous;
|
|
4269
4864
|
}
|
|
4270
4865
|
});
|
|
4271
|
-
write([
|
|
4866
|
+
write([`Watching .penv/ and penv.config.ts. ${out.dim("Ctrl-C to stop.")}`]);
|
|
4272
4867
|
await new Promise((resolve7) => {
|
|
4273
4868
|
process.once("SIGINT", () => {
|
|
4274
4869
|
handle.close();
|