@penvhq/cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1042 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -7
- package/dist/index.d.ts +165 -7
- package/dist/index.js +986 -197
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -1,43 +1,76 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { setKeychain } from "@penvhq/core";
|
|
3
|
-
import { runMain as cittyRunMain, defineCommand as
|
|
3
|
+
import { runMain as cittyRunMain, defineCommand as defineCommand18 } from "citty";
|
|
4
4
|
|
|
5
5
|
// src/commands/doctor.ts
|
|
6
6
|
import {
|
|
7
7
|
accessPath as accessPath3,
|
|
8
|
+
assertNever,
|
|
8
9
|
effectiveMeta,
|
|
10
|
+
formatValueFile as formatValueFile2,
|
|
9
11
|
isPublicVariable,
|
|
10
12
|
isRequired,
|
|
11
13
|
isSecret,
|
|
14
|
+
isStuck,
|
|
15
|
+
openValue as openValue2,
|
|
12
16
|
resolveAll as resolveAll2,
|
|
17
|
+
rotationOf,
|
|
18
|
+
tryParseDuration,
|
|
13
19
|
variableName as variableName3
|
|
14
20
|
} from "@penvhq/core";
|
|
15
21
|
import { createGithubSink as createGithubSink2 } from "@penvhq/sink-github";
|
|
16
22
|
import { defineCommand as defineCommand3 } from "citty";
|
|
17
23
|
|
|
18
24
|
// src/project.ts
|
|
19
|
-
import { dirname, resolve } from "path";
|
|
25
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
20
26
|
import {
|
|
21
27
|
candidatesFor,
|
|
22
28
|
formatValueFile,
|
|
29
|
+
isCanonicalSegment,
|
|
23
30
|
isReservedToken,
|
|
24
31
|
loadConfig,
|
|
25
32
|
openValue,
|
|
26
33
|
PenvError as PenvError2,
|
|
27
34
|
parameterId,
|
|
28
35
|
ReservedTokenError,
|
|
36
|
+
refFromAccessPath,
|
|
29
37
|
resolveEnvironment,
|
|
30
38
|
resolveKeySource
|
|
31
39
|
} from "@penvhq/core";
|
|
32
40
|
import { FilesystemProvider } from "@penvhq/provider-filesystem";
|
|
33
41
|
|
|
34
42
|
// src/registry.ts
|
|
43
|
+
import { createRequire } from "module";
|
|
44
|
+
import { dirname, resolve } from "path";
|
|
45
|
+
import { pathToFileURL } from "url";
|
|
35
46
|
import { PenvError } from "@penvhq/core";
|
|
36
47
|
import { createFilesystemProvider } from "@penvhq/provider-filesystem";
|
|
48
|
+
import { createKubernetesProvider } from "@penvhq/provider-kubernetes";
|
|
49
|
+
import { createMockProvider } from "@penvhq/provider-mock";
|
|
50
|
+
import { createSsmProvider } from "@penvhq/provider-ssm";
|
|
51
|
+
import { createVaultProvider } from "@penvhq/provider-vault";
|
|
52
|
+
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
37
53
|
var LOCAL_TREE_TYPE = "filesystem";
|
|
38
54
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
39
|
-
[LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })]
|
|
55
|
+
[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
|
+
[
|
|
59
|
+
"kubernetes",
|
|
60
|
+
({ providerConfig }) => createKubernetesProvider(kubernetesOptions(providerConfig))
|
|
61
|
+
],
|
|
62
|
+
["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
|
|
40
63
|
]);
|
|
64
|
+
var CONTRACT_METHODS = [
|
|
65
|
+
"read",
|
|
66
|
+
"write",
|
|
67
|
+
"list",
|
|
68
|
+
"remove",
|
|
69
|
+
"readMeta",
|
|
70
|
+
"writeMeta",
|
|
71
|
+
"removeMeta"
|
|
72
|
+
];
|
|
73
|
+
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
41
74
|
function isProviderRegistered(type) {
|
|
42
75
|
return REGISTRY.has(type);
|
|
43
76
|
}
|
|
@@ -48,20 +81,102 @@ function createProvider(type, context) {
|
|
|
48
81
|
}
|
|
49
82
|
return factory(context);
|
|
50
83
|
}
|
|
51
|
-
function
|
|
84
|
+
async function createSourceProvider(type, context) {
|
|
85
|
+
if (REGISTRY.has(type)) {
|
|
86
|
+
return createProvider(type, context);
|
|
87
|
+
}
|
|
88
|
+
return loadPluginProvider(type, context);
|
|
89
|
+
}
|
|
90
|
+
async function loadPluginProvider(type, context) {
|
|
91
|
+
const fromDir = resolutionBase(context);
|
|
92
|
+
const specifier = pluginSpecifier(context.providerConfig, type);
|
|
93
|
+
const resolved = resolvePlugin(specifier, fromDir);
|
|
94
|
+
if (resolved === void 0) {
|
|
95
|
+
throw unknownProvider(type, context.environment, specifier);
|
|
96
|
+
}
|
|
97
|
+
let mod;
|
|
98
|
+
try {
|
|
99
|
+
mod = await importPlugin(resolved);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new PenvError(
|
|
102
|
+
"PROVIDER_PLUGIN_LOAD",
|
|
103
|
+
`The provider package \`${specifier}\` for type \`${type}\` failed to load`,
|
|
104
|
+
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
108
|
+
if (typeof factory !== "function") {
|
|
109
|
+
throw new PenvError(
|
|
110
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
111
|
+
`\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
112
|
+
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const provider = await factory(context);
|
|
116
|
+
assertSatisfiesContract(provider, specifier);
|
|
117
|
+
return provider;
|
|
118
|
+
}
|
|
119
|
+
function assertProvidersRegistered(config, projectRoot) {
|
|
52
120
|
for (const [environment, provider] of Object.entries(config.providers)) {
|
|
53
|
-
if (
|
|
54
|
-
|
|
121
|
+
if (isProviderRegistered(provider.type)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const specifier = pluginSpecifier(provider, provider.type);
|
|
125
|
+
if (resolvePlugin(specifier, projectRoot) === void 0) {
|
|
126
|
+
throw unknownProvider(provider.type, environment, specifier);
|
|
55
127
|
}
|
|
56
128
|
}
|
|
57
129
|
}
|
|
58
|
-
function
|
|
130
|
+
function pluginSpecifier(providerConfig, type) {
|
|
131
|
+
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
132
|
+
}
|
|
133
|
+
function resolutionBase(context) {
|
|
134
|
+
return dirname(context.root);
|
|
135
|
+
}
|
|
136
|
+
function resolvePlugin(specifier, fromDir) {
|
|
137
|
+
try {
|
|
138
|
+
const require2 = createRequire(resolve(fromDir, "noop.js"));
|
|
139
|
+
return require2.resolve(specifier);
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function importPlugin(resolvedPath) {
|
|
145
|
+
const cached2 = pluginModuleCache.get(resolvedPath);
|
|
146
|
+
if (cached2 !== void 0) {
|
|
147
|
+
return cached2;
|
|
148
|
+
}
|
|
149
|
+
const loading = import(pathToFileURL(resolvedPath).href);
|
|
150
|
+
pluginModuleCache.set(resolvedPath, loading);
|
|
151
|
+
return loading;
|
|
152
|
+
}
|
|
153
|
+
function assertSatisfiesContract(provider, specifier) {
|
|
154
|
+
for (const method of CONTRACT_METHODS) {
|
|
155
|
+
if (typeof provider[method] !== "function") {
|
|
156
|
+
throw new PenvError(
|
|
157
|
+
"PROVIDER_PLUGIN_INVALID",
|
|
158
|
+
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
159
|
+
"It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function kubernetesOptions(providerConfig) {
|
|
165
|
+
const path = providerConfig?.path ?? "penv";
|
|
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) {
|
|
59
173
|
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
60
174
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
175
|
+
const remedy = specifier === void 0 ? `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.` : `Install its package with \`npm i ${specifier}\`, or name a built-in provider: ${known}.`;
|
|
61
176
|
return new PenvError(
|
|
62
177
|
"UNKNOWN_PROVIDER",
|
|
63
178
|
`The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
|
|
64
|
-
|
|
179
|
+
remedy
|
|
65
180
|
);
|
|
66
181
|
}
|
|
67
182
|
|
|
@@ -69,9 +184,9 @@ function unknownProvider(type, environment) {
|
|
|
69
184
|
var PENV_DIR = ".penv";
|
|
70
185
|
function openProject(cwd) {
|
|
71
186
|
const { config, file } = loadConfig(cwd);
|
|
72
|
-
const root =
|
|
73
|
-
const penvDir =
|
|
74
|
-
assertProvidersRegistered(config);
|
|
187
|
+
const root = dirname2(file);
|
|
188
|
+
const penvDir = resolve2(root, PENV_DIR);
|
|
189
|
+
assertProvidersRegistered(config, root);
|
|
75
190
|
return {
|
|
76
191
|
root,
|
|
77
192
|
configFile: file,
|
|
@@ -90,6 +205,18 @@ function localTree(project) {
|
|
|
90
205
|
}
|
|
91
206
|
return project.provider;
|
|
92
207
|
}
|
|
208
|
+
async function sourceProviderFor(project, environment) {
|
|
209
|
+
const providerConfig = project.config.providers[environment];
|
|
210
|
+
if (providerConfig === void 0) {
|
|
211
|
+
return createProvider(LOCAL_TREE_TYPE, { root: project.penvDir, config: project.config });
|
|
212
|
+
}
|
|
213
|
+
return createSourceProvider(providerConfig.type, {
|
|
214
|
+
root: project.penvDir,
|
|
215
|
+
config: project.config,
|
|
216
|
+
providerConfig,
|
|
217
|
+
environment
|
|
218
|
+
});
|
|
219
|
+
}
|
|
93
220
|
function targetEnvironment(project, explicit) {
|
|
94
221
|
return resolveEnvironment(project.config, explicit);
|
|
95
222
|
}
|
|
@@ -110,6 +237,26 @@ function refFromKey(key, config) {
|
|
|
110
237
|
}
|
|
111
238
|
return { namespace: segments.slice(0, -1), name };
|
|
112
239
|
}
|
|
240
|
+
function assertWritableKey(key) {
|
|
241
|
+
const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
|
|
242
|
+
if (segments.length === 0 || segments.every((s) => isCanonicalSegment(s))) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const ref = refFromAccessPath(segments);
|
|
246
|
+
if (ref !== void 0) {
|
|
247
|
+
const suggestion = [...ref.namespace, ref.name].join("/");
|
|
248
|
+
throw new PenvError2(
|
|
249
|
+
"PARAMETER_KEY_CASING",
|
|
250
|
+
`\`${key}\` is not a canonical parameter name`,
|
|
251
|
+
`Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
throw new PenvError2(
|
|
255
|
+
"PARAMETER_KEY_UNREACHABLE",
|
|
256
|
+
`No value file can be named that reaches \`${key}\``,
|
|
257
|
+
"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."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
113
260
|
function keySourceFor(project, environment) {
|
|
114
261
|
return resolveKeySource(project.config, environment);
|
|
115
262
|
}
|
|
@@ -156,7 +303,7 @@ import {
|
|
|
156
303
|
accessPath,
|
|
157
304
|
isReservedToken as isReservedToken2,
|
|
158
305
|
parameterId as parameterId2,
|
|
159
|
-
refFromAccessPath,
|
|
306
|
+
refFromAccessPath as refFromAccessPath2,
|
|
160
307
|
variableName
|
|
161
308
|
} from "@penvhq/core";
|
|
162
309
|
function defOf(node) {
|
|
@@ -264,7 +411,7 @@ function computeDrift(input) {
|
|
|
264
411
|
continue;
|
|
265
412
|
}
|
|
266
413
|
const path = leaf.path.join(".");
|
|
267
|
-
const ref =
|
|
414
|
+
const ref = refFromAccessPath2(leaf.path);
|
|
268
415
|
if (ref === void 0 || isReservedToken2(ref.name, config)) {
|
|
269
416
|
declared.push({
|
|
270
417
|
subject: path,
|
|
@@ -471,36 +618,36 @@ async function runPush(options) {
|
|
|
471
618
|
decrypted: outbound.filter((item) => item.encrypted).length
|
|
472
619
|
};
|
|
473
620
|
}
|
|
474
|
-
function renderPush(
|
|
475
|
-
if (
|
|
621
|
+
function renderPush(result2) {
|
|
622
|
+
if (result2.pushed === 0) {
|
|
476
623
|
return formatRows([
|
|
477
624
|
{
|
|
478
625
|
glyph: CHECK,
|
|
479
626
|
label: "Nothing to push",
|
|
480
|
-
subject: `no values resolve for environment ${
|
|
627
|
+
subject: `no values resolve for environment ${result2.environment}`
|
|
481
628
|
}
|
|
482
629
|
]);
|
|
483
630
|
}
|
|
484
|
-
const target = `GitHub Actions for environment ${
|
|
631
|
+
const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
|
|
485
632
|
const rows = [
|
|
486
633
|
{
|
|
487
634
|
glyph: CHECK,
|
|
488
635
|
label: "Pushed",
|
|
489
|
-
subject: `${
|
|
636
|
+
subject: `${result2.pushed} ${result2.pushed === 1 ? "secret" : "secrets"}`,
|
|
490
637
|
detail: `to ${target}`
|
|
491
638
|
},
|
|
492
639
|
{
|
|
493
640
|
glyph: CHECK,
|
|
494
641
|
label: "Scopes",
|
|
495
|
-
subject: `${
|
|
642
|
+
subject: `${result2.environmentSecrets} environment, ${result2.repositorySecrets} repository`,
|
|
496
643
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
497
644
|
}
|
|
498
645
|
];
|
|
499
|
-
if (
|
|
646
|
+
if (result2.decrypted > 0) {
|
|
500
647
|
rows.push({
|
|
501
648
|
glyph: WARN,
|
|
502
649
|
label: "Decrypted",
|
|
503
|
-
subject: `${
|
|
650
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
504
651
|
detail: "sent as plaintext for GitHub to re-seal under its own key"
|
|
505
652
|
});
|
|
506
653
|
}
|
|
@@ -520,19 +667,19 @@ var pushCommand = defineCommand({
|
|
|
520
667
|
},
|
|
521
668
|
run({ args }) {
|
|
522
669
|
return guard(async () => {
|
|
523
|
-
const
|
|
670
|
+
const result2 = await runPush({
|
|
524
671
|
cwd: process.cwd(),
|
|
525
672
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
526
673
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
527
674
|
});
|
|
528
|
-
write(renderPush(
|
|
675
|
+
write(renderPush(result2));
|
|
529
676
|
});
|
|
530
677
|
}
|
|
531
678
|
});
|
|
532
679
|
|
|
533
680
|
// src/commands/validate.ts
|
|
534
681
|
import { resolve as resolvePath } from "path";
|
|
535
|
-
import { pathToFileURL } from "url";
|
|
682
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
536
683
|
import {
|
|
537
684
|
accessPath as accessPath2,
|
|
538
685
|
checkNameCollisions as checkNameCollisions2,
|
|
@@ -610,16 +757,16 @@ function validationIssuesOf(cause, schemaPath) {
|
|
|
610
757
|
}
|
|
611
758
|
var schemaLoads = Promise.resolve();
|
|
612
759
|
function exclusively(work) {
|
|
613
|
-
const
|
|
614
|
-
schemaLoads =
|
|
760
|
+
const result2 = schemaLoads.then(work, work);
|
|
761
|
+
schemaLoads = result2.then(
|
|
615
762
|
() => void 0,
|
|
616
763
|
() => void 0
|
|
617
764
|
);
|
|
618
|
-
return
|
|
765
|
+
return result2;
|
|
619
766
|
}
|
|
620
767
|
function loadSchema(project, environment) {
|
|
621
768
|
const schemaPath = schemaFileOf(project.config);
|
|
622
|
-
const file =
|
|
769
|
+
const file = pathToFileURL2(resolvePath(project.root, schemaPath)).href;
|
|
623
770
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
624
771
|
}
|
|
625
772
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
@@ -721,9 +868,9 @@ async function runValidate(options) {
|
|
|
721
868
|
}
|
|
722
869
|
}
|
|
723
870
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
724
|
-
const
|
|
725
|
-
if (!
|
|
726
|
-
for (const problem of
|
|
871
|
+
const result2 = schema.safeParse(object);
|
|
872
|
+
if (!result2.success) {
|
|
873
|
+
for (const problem of result2.error.issues) {
|
|
727
874
|
const path = problem.path.join(".");
|
|
728
875
|
if (undecryptable.has(path)) {
|
|
729
876
|
continue;
|
|
@@ -739,24 +886,24 @@ async function runValidate(options) {
|
|
|
739
886
|
}
|
|
740
887
|
return { ok: issues.length === 0, environment, parameters: refs.length, issues, drift };
|
|
741
888
|
}
|
|
742
|
-
function renderValidate(
|
|
743
|
-
if (
|
|
889
|
+
function renderValidate(result2) {
|
|
890
|
+
if (result2.ok) {
|
|
744
891
|
return formatRows([
|
|
745
892
|
{
|
|
746
893
|
glyph: CHECK,
|
|
747
894
|
label: "Schema valid",
|
|
748
|
-
subject: `${
|
|
895
|
+
subject: `${result2.parameters} parameters for environment ${result2.environment}`
|
|
749
896
|
}
|
|
750
897
|
]);
|
|
751
898
|
}
|
|
752
|
-
const rows =
|
|
899
|
+
const rows = result2.issues.map((issue) => ({
|
|
753
900
|
glyph: WARN,
|
|
754
901
|
label: LABELS[issue.kind],
|
|
755
902
|
subject: issue.subject,
|
|
756
903
|
detail: issue.message
|
|
757
904
|
}));
|
|
758
905
|
const lines = formatRows(rows);
|
|
759
|
-
const remedies = [...new Set(
|
|
906
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
|
|
760
907
|
for (const remedy of remedies) {
|
|
761
908
|
if (remedy !== void 0) {
|
|
762
909
|
lines.push(` ${remedy}`);
|
|
@@ -774,12 +921,12 @@ var validateCommand = defineCommand2({
|
|
|
774
921
|
},
|
|
775
922
|
run({ args }) {
|
|
776
923
|
return guard(async () => {
|
|
777
|
-
const
|
|
924
|
+
const result2 = await runValidate({
|
|
778
925
|
cwd: process.cwd(),
|
|
779
926
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
780
927
|
});
|
|
781
|
-
write(renderValidate(
|
|
782
|
-
if (!
|
|
928
|
+
write(renderValidate(result2));
|
|
929
|
+
if (!result2.ok) {
|
|
783
930
|
process.exitCode = 1;
|
|
784
931
|
}
|
|
785
932
|
});
|
|
@@ -787,6 +934,7 @@ var validateCommand = defineCommand2({
|
|
|
787
934
|
});
|
|
788
935
|
|
|
789
936
|
// src/commands/doctor.ts
|
|
937
|
+
var STUCK_THRESHOLD_MS = 864e5;
|
|
790
938
|
function skipped(check, label) {
|
|
791
939
|
return {
|
|
792
940
|
check,
|
|
@@ -800,6 +948,8 @@ async function runDoctor(options) {
|
|
|
800
948
|
const project = openProject(options.cwd);
|
|
801
949
|
const environment = targetEnvironment(project, options.environment);
|
|
802
950
|
const findings = [];
|
|
951
|
+
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
952
|
+
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
803
953
|
const { schema, issues } = await loadSchema(project, environment);
|
|
804
954
|
if (schema !== void 0) {
|
|
805
955
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -848,6 +998,14 @@ async function runDoctor(options) {
|
|
|
848
998
|
findings.push(...plaintextSecretFindings(subjects, environment));
|
|
849
999
|
findings.push(...publicSecretFindings(subjects, environment, project.config));
|
|
850
1000
|
findings.push(...encryptionFindings(subjects, environment));
|
|
1001
|
+
const rotation = await rotationSubjects(project, environment, subjects, options.source);
|
|
1002
|
+
if (rotation.kind === "unreachable") {
|
|
1003
|
+
findings.push(rotation.finding);
|
|
1004
|
+
} else {
|
|
1005
|
+
findings.push(...overdueFindings(rotation.subjects, environment, now));
|
|
1006
|
+
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1007
|
+
}
|
|
1008
|
+
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
851
1009
|
findings.push(...await sinkFindings(project, environment, options.sink));
|
|
852
1010
|
findings.push({
|
|
853
1011
|
check: "provider",
|
|
@@ -1265,6 +1423,246 @@ async function sinkFindings(project, environment, override) {
|
|
|
1265
1423
|
});
|
|
1266
1424
|
return findings;
|
|
1267
1425
|
}
|
|
1426
|
+
function humanizeMs(ms) {
|
|
1427
|
+
const abs = Math.max(0, ms);
|
|
1428
|
+
const day = 864e5;
|
|
1429
|
+
const hour = 36e5;
|
|
1430
|
+
const minute = 6e4;
|
|
1431
|
+
const round = (value, unit) => {
|
|
1432
|
+
const n = Math.round(value);
|
|
1433
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
1434
|
+
};
|
|
1435
|
+
if (abs >= day) return round(abs / day, "day");
|
|
1436
|
+
if (abs >= hour) return round(abs / hour, "hour");
|
|
1437
|
+
if (abs >= minute) return round(abs / minute, "minute");
|
|
1438
|
+
return round(abs / 1e3, "second");
|
|
1439
|
+
}
|
|
1440
|
+
function refPathOf(ref) {
|
|
1441
|
+
return [...ref.namespace, ref.name].join("/");
|
|
1442
|
+
}
|
|
1443
|
+
async function rotationSubjects(project, environment, local, override) {
|
|
1444
|
+
const providerConfig = project.config.providers[environment];
|
|
1445
|
+
if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
|
|
1446
|
+
return { kind: "read", subjects: local };
|
|
1447
|
+
}
|
|
1448
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1449
|
+
try {
|
|
1450
|
+
const subjects = await Promise.all(
|
|
1451
|
+
local.map(async ({ resolution }) => ({
|
|
1452
|
+
resolution,
|
|
1453
|
+
meta: await source.readMeta(resolution.ref)
|
|
1454
|
+
}))
|
|
1455
|
+
);
|
|
1456
|
+
return { kind: "read", subjects };
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
return {
|
|
1459
|
+
kind: "unreachable",
|
|
1460
|
+
finding: {
|
|
1461
|
+
check: "rotation-overdue",
|
|
1462
|
+
severity: "unknown",
|
|
1463
|
+
label: "Rotation",
|
|
1464
|
+
subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
|
|
1465
|
+
detail: errorDetail(error)
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function overdueFindings(subjects, environment, now) {
|
|
1471
|
+
const findings = [];
|
|
1472
|
+
for (const { resolution, meta } of subjects) {
|
|
1473
|
+
const { policy, lastRotated } = rotationOf(meta, environment);
|
|
1474
|
+
if (policy === void 0 || lastRotated === null) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
const interval = tryParseDuration(policy);
|
|
1478
|
+
if (interval === void 0) {
|
|
1479
|
+
findings.push({
|
|
1480
|
+
check: "rotation-overdue",
|
|
1481
|
+
severity: "warning",
|
|
1482
|
+
label: "Rotation policy invalid",
|
|
1483
|
+
subject: resolution.parameter,
|
|
1484
|
+
detail: `rotationPolicy \`${policy}\` is not a duration penv can parse (e.g. \`90d\`, \`24h\`)`
|
|
1485
|
+
});
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const last = Date.parse(lastRotated);
|
|
1489
|
+
if (Number.isNaN(last)) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const overdueBy = now.getTime() - last - interval;
|
|
1493
|
+
if (overdueBy <= 0) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
findings.push({
|
|
1497
|
+
check: "rotation-overdue",
|
|
1498
|
+
severity: "warning",
|
|
1499
|
+
label: "Rotation overdue",
|
|
1500
|
+
subject: resolution.parameter,
|
|
1501
|
+
detail: `overdue by ~${humanizeMs(overdueBy)}, policy ${policy}`,
|
|
1502
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --env ${environment}`
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
if (findings.length > 0) {
|
|
1506
|
+
return findings;
|
|
1507
|
+
}
|
|
1508
|
+
return [
|
|
1509
|
+
{
|
|
1510
|
+
check: "rotation-overdue",
|
|
1511
|
+
severity: "pass",
|
|
1512
|
+
label: "Rotation freshness",
|
|
1513
|
+
subject: `no parameter is past its rotation policy for ${environment}`
|
|
1514
|
+
}
|
|
1515
|
+
];
|
|
1516
|
+
}
|
|
1517
|
+
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1518
|
+
const findings = [];
|
|
1519
|
+
for (const { resolution, meta } of subjects) {
|
|
1520
|
+
if (!isStuck(meta, environment, now, stuckThresholdMs)) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const { rotatingSince } = rotationOf(meta, environment);
|
|
1524
|
+
if (rotatingSince === null) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const openFor = now.getTime() - Date.parse(rotatingSince);
|
|
1528
|
+
findings.push({
|
|
1529
|
+
check: "rotation-stuck",
|
|
1530
|
+
severity: "warning",
|
|
1531
|
+
label: "Rotation stuck",
|
|
1532
|
+
subject: resolution.parameter,
|
|
1533
|
+
detail: `dual-valid window open ~${humanizeMs(openFor)}, past the ${humanizeMs(stuckThresholdMs)} grace window`,
|
|
1534
|
+
remedy: `penv rotate ${refPathOf(resolution.ref)} --complete --env ${environment}`
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
if (findings.length > 0) {
|
|
1538
|
+
return findings;
|
|
1539
|
+
}
|
|
1540
|
+
return [
|
|
1541
|
+
{
|
|
1542
|
+
check: "rotation-stuck",
|
|
1543
|
+
severity: "pass",
|
|
1544
|
+
label: "Rotation progress",
|
|
1545
|
+
subject: `no dual-valid rotation has stayed open past its grace window for ${environment}`
|
|
1546
|
+
}
|
|
1547
|
+
];
|
|
1548
|
+
}
|
|
1549
|
+
function driftKey(file) {
|
|
1550
|
+
return [file.namespace.join("/"), file.name, scopeKey(file.scope)].join(" ");
|
|
1551
|
+
}
|
|
1552
|
+
function scopeKey(scope) {
|
|
1553
|
+
switch (scope.kind) {
|
|
1554
|
+
case "unscoped":
|
|
1555
|
+
return "unscoped";
|
|
1556
|
+
case "environment":
|
|
1557
|
+
return `environment:${scope.environment}`;
|
|
1558
|
+
case "local":
|
|
1559
|
+
return "local";
|
|
1560
|
+
case "environment-local":
|
|
1561
|
+
return `environment-local:${scope.environment}`;
|
|
1562
|
+
default:
|
|
1563
|
+
return assertNever(scope, "scope");
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
function relevantToEnvironment(file, environment) {
|
|
1567
|
+
const scope = file.scope;
|
|
1568
|
+
if (scope.kind === "unscoped") return true;
|
|
1569
|
+
if (scope.kind === "environment") return scope.environment === environment;
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
async function readRelevant(provider, environment) {
|
|
1573
|
+
const files = (await provider.list()).filter((file) => relevantToEnvironment(file, environment));
|
|
1574
|
+
const stored = await Promise.all(files.map((file) => provider.read(file)));
|
|
1575
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1576
|
+
for (const [index, file] of files.entries()) {
|
|
1577
|
+
const value = stored[index];
|
|
1578
|
+
if (value !== void 0) {
|
|
1579
|
+
entries.set(driftKey(file), { file, stored: value });
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return entries;
|
|
1583
|
+
}
|
|
1584
|
+
async function providerDriftFindings(project, environment, override) {
|
|
1585
|
+
const providerConfig = project.config.providers[environment];
|
|
1586
|
+
if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
|
|
1587
|
+
return [
|
|
1588
|
+
{
|
|
1589
|
+
check: "provider-value-drift",
|
|
1590
|
+
severity: "pass",
|
|
1591
|
+
label: "Provider values",
|
|
1592
|
+
subject: `${environment} keeps its values in the local .penv tree, so there is no other source of truth to compare against`
|
|
1593
|
+
}
|
|
1594
|
+
];
|
|
1595
|
+
}
|
|
1596
|
+
const source = override ?? await sourceProviderFor(project, environment);
|
|
1597
|
+
let local;
|
|
1598
|
+
let remote;
|
|
1599
|
+
try {
|
|
1600
|
+
local = await readRelevant(project.provider, environment);
|
|
1601
|
+
remote = await readRelevant(source, environment);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
return [
|
|
1604
|
+
{
|
|
1605
|
+
check: "provider-value-drift",
|
|
1606
|
+
severity: "unknown",
|
|
1607
|
+
label: "Provider values",
|
|
1608
|
+
subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
|
|
1609
|
+
detail: errorDetail(error)
|
|
1610
|
+
}
|
|
1611
|
+
];
|
|
1612
|
+
}
|
|
1613
|
+
const keys = keySourceFor(project, environment);
|
|
1614
|
+
const findings = [];
|
|
1615
|
+
const addresses = [.../* @__PURE__ */ new Set([...local.keys(), ...remote.keys()])].sort();
|
|
1616
|
+
for (const address of addresses) {
|
|
1617
|
+
const here = local.get(address);
|
|
1618
|
+
const there = remote.get(address);
|
|
1619
|
+
if (here !== void 0 && there !== void 0) {
|
|
1620
|
+
const opened = openValue2(here.file, here.stored, keys);
|
|
1621
|
+
if (opened.kind !== "plaintext") {
|
|
1622
|
+
findings.push({
|
|
1623
|
+
check: "provider-value-drift",
|
|
1624
|
+
severity: "unknown",
|
|
1625
|
+
label: "Provider value unreadable",
|
|
1626
|
+
subject: formatValueFile2(here.file),
|
|
1627
|
+
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1628
|
+
});
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
if (opened.value !== there.stored) {
|
|
1632
|
+
findings.push({
|
|
1633
|
+
check: "provider-value-drift",
|
|
1634
|
+
severity: "failure",
|
|
1635
|
+
label: "Provider value drift",
|
|
1636
|
+
subject: formatValueFile2(here.file),
|
|
1637
|
+
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const present = here ?? there;
|
|
1643
|
+
if (present === void 0) {
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
findings.push({
|
|
1647
|
+
check: "provider-value-drift",
|
|
1648
|
+
severity: "warning",
|
|
1649
|
+
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1650
|
+
subject: formatValueFile2(present.file),
|
|
1651
|
+
detail: here !== void 0 ? `present locally, absent from the ${providerConfig.type} source of truth` : `present in the ${providerConfig.type} source of truth, absent from the local tree`
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
if (findings.length > 0) {
|
|
1655
|
+
return findings;
|
|
1656
|
+
}
|
|
1657
|
+
return [
|
|
1658
|
+
{
|
|
1659
|
+
check: "provider-value-drift",
|
|
1660
|
+
severity: "pass",
|
|
1661
|
+
label: "Provider values",
|
|
1662
|
+
subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
|
|
1663
|
+
}
|
|
1664
|
+
];
|
|
1665
|
+
}
|
|
1268
1666
|
function renderDoctor(report) {
|
|
1269
1667
|
const rows = report.findings.map((finding) => ({
|
|
1270
1668
|
glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
|
|
@@ -1307,9 +1705,9 @@ var doctorCommand = defineCommand3({
|
|
|
1307
1705
|
|
|
1308
1706
|
// src/commands/encrypt.ts
|
|
1309
1707
|
import {
|
|
1310
|
-
formatValueFile as
|
|
1708
|
+
formatValueFile as formatValueFile4,
|
|
1311
1709
|
isSecret as isSecret3,
|
|
1312
|
-
openValue as
|
|
1710
|
+
openValue as openValue3,
|
|
1313
1711
|
PenvError as PenvError7,
|
|
1314
1712
|
parameterId as parameterId4,
|
|
1315
1713
|
sealValue as sealValue2
|
|
@@ -1317,7 +1715,7 @@ import {
|
|
|
1317
1715
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1318
1716
|
|
|
1319
1717
|
// src/commands/set.ts
|
|
1320
|
-
import { formatValueFile as
|
|
1718
|
+
import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
|
|
1321
1719
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1322
1720
|
function scopeFrom(options) {
|
|
1323
1721
|
if (options.local === true) {
|
|
@@ -1352,38 +1750,51 @@ function sealFor(project, file, value, parameter, environment) {
|
|
|
1352
1750
|
if (environment === void 0) {
|
|
1353
1751
|
throw new PenvError6(
|
|
1354
1752
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1355
|
-
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${
|
|
1753
|
+
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
|
|
1356
1754
|
"Keys are declared per environment in the `keys` block of penv.config.ts, so penv cannot tell which key should seal a file that every environment reads. Write it at an environment scope \u2014 add `--env <environment>` \u2014 or drop `secret` from the parameter's meta."
|
|
1357
1755
|
);
|
|
1358
1756
|
}
|
|
1359
1757
|
return sealValue(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1360
1758
|
}
|
|
1361
|
-
async function
|
|
1362
|
-
const project =
|
|
1363
|
-
const
|
|
1364
|
-
const scope = targetScope(project, options, options.key);
|
|
1365
|
-
const environment = policyEnvironment(project, options);
|
|
1366
|
-
const secret = isSecret2(await project.provider.readMeta(ref), environment);
|
|
1759
|
+
async function sealAwareWrite(options) {
|
|
1760
|
+
const { project, provider, ref, scope, value, environment } = options;
|
|
1761
|
+
const secret = isSecret2(await provider.readMeta(ref), environment);
|
|
1367
1762
|
const file = {
|
|
1368
1763
|
namespace: ref.namespace,
|
|
1369
1764
|
name: ref.name,
|
|
1370
1765
|
scope,
|
|
1371
1766
|
encrypted: secret
|
|
1372
1767
|
};
|
|
1373
|
-
const stored = secret ? sealFor(project, file,
|
|
1374
|
-
await
|
|
1375
|
-
await
|
|
1376
|
-
return {
|
|
1768
|
+
const stored = secret ? sealFor(project, file, value, parameterId3(ref), environment) : value;
|
|
1769
|
+
await provider.write(file, stored);
|
|
1770
|
+
await provider.remove({ ...file, encrypted: !secret });
|
|
1771
|
+
return { encrypted: secret, location: formatValueFile3(file) };
|
|
1772
|
+
}
|
|
1773
|
+
async function runSet(options) {
|
|
1774
|
+
const project = openProject(options.cwd);
|
|
1775
|
+
assertWritableKey(options.key);
|
|
1776
|
+
const ref = refFromKey(options.key, project.config);
|
|
1777
|
+
const scope = targetScope(project, options, options.key);
|
|
1778
|
+
const environment = policyEnvironment(project, options);
|
|
1779
|
+
const { encrypted, location } = await sealAwareWrite({
|
|
1780
|
+
project,
|
|
1781
|
+
provider: project.provider,
|
|
1782
|
+
ref,
|
|
1783
|
+
scope,
|
|
1784
|
+
value: options.value,
|
|
1785
|
+
environment
|
|
1786
|
+
});
|
|
1787
|
+
return { parameter: options.key, location, encrypted };
|
|
1377
1788
|
}
|
|
1378
|
-
function renderSet(
|
|
1789
|
+
function renderSet(result2) {
|
|
1379
1790
|
return formatRows([
|
|
1380
1791
|
{
|
|
1381
1792
|
glyph: CHECK,
|
|
1382
1793
|
label: "Wrote",
|
|
1383
|
-
subject: `${PENV_DIR}/${
|
|
1794
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1384
1795
|
// The `.enc` suffix says this already, but only to a reader who knows the
|
|
1385
1796
|
// grammar. Meta decided it, not the command line, so the command says so.
|
|
1386
|
-
...
|
|
1797
|
+
...result2.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
1387
1798
|
}
|
|
1388
1799
|
]);
|
|
1389
1800
|
}
|
|
@@ -1460,7 +1871,7 @@ async function runEncrypt(options) {
|
|
|
1460
1871
|
const already = await readOne(project, sealed);
|
|
1461
1872
|
throw new PenvError7(
|
|
1462
1873
|
"PARAMETER_ABSENT",
|
|
1463
|
-
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${
|
|
1874
|
+
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile4(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
1464
1875
|
already === void 0 ? `Write it first with \`penv set ${options.key} --env ${environment}\`, which seals it automatically when the parameter's meta declares it a secret.` : "Nothing to do."
|
|
1465
1876
|
);
|
|
1466
1877
|
}
|
|
@@ -1469,8 +1880,8 @@ async function runEncrypt(options) {
|
|
|
1469
1880
|
await project.provider.remove(plain);
|
|
1470
1881
|
return {
|
|
1471
1882
|
parameter,
|
|
1472
|
-
location:
|
|
1473
|
-
removed:
|
|
1883
|
+
location: formatValueFile4(sealed),
|
|
1884
|
+
removed: formatValueFile4(plain)
|
|
1474
1885
|
};
|
|
1475
1886
|
}
|
|
1476
1887
|
async function runDecrypt(options) {
|
|
@@ -1489,16 +1900,16 @@ async function runDecrypt(options) {
|
|
|
1489
1900
|
if (stored === void 0) {
|
|
1490
1901
|
throw new PenvError7(
|
|
1491
1902
|
"PARAMETER_ABSENT",
|
|
1492
|
-
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${
|
|
1903
|
+
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
|
|
1493
1904
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
1494
1905
|
);
|
|
1495
1906
|
}
|
|
1496
|
-
const opened =
|
|
1907
|
+
const opened = openValue3(sealed, stored, keySourceFor(project, environment));
|
|
1497
1908
|
if (opened.kind === "failed") {
|
|
1498
1909
|
throw new UndecryptableAt(
|
|
1499
1910
|
parameter,
|
|
1500
1911
|
environment,
|
|
1501
|
-
|
|
1912
|
+
formatValueFile4(sealed),
|
|
1502
1913
|
opened.failure.detail
|
|
1503
1914
|
);
|
|
1504
1915
|
}
|
|
@@ -1506,8 +1917,8 @@ async function runDecrypt(options) {
|
|
|
1506
1917
|
await project.provider.remove(sealed);
|
|
1507
1918
|
return {
|
|
1508
1919
|
parameter,
|
|
1509
|
-
location:
|
|
1510
|
-
removed:
|
|
1920
|
+
location: formatValueFile4(plain),
|
|
1921
|
+
removed: formatValueFile4(sealed)
|
|
1511
1922
|
};
|
|
1512
1923
|
}
|
|
1513
1924
|
var UndecryptableAt = class extends PenvError7 {
|
|
@@ -1519,13 +1930,13 @@ var UndecryptableAt = class extends PenvError7 {
|
|
|
1519
1930
|
);
|
|
1520
1931
|
}
|
|
1521
1932
|
};
|
|
1522
|
-
function renderReseal(
|
|
1933
|
+
function renderReseal(result2, verb) {
|
|
1523
1934
|
return formatRows([
|
|
1524
1935
|
{
|
|
1525
1936
|
glyph: CHECK,
|
|
1526
1937
|
label: verb,
|
|
1527
|
-
subject: `${PENV_DIR}/${
|
|
1528
|
-
detail: `${PENV_DIR}/${
|
|
1938
|
+
subject: `${PENV_DIR}/${result2.location}`,
|
|
1939
|
+
detail: `${PENV_DIR}/${result2.removed} removed`
|
|
1529
1940
|
}
|
|
1530
1941
|
]);
|
|
1531
1942
|
}
|
|
@@ -1548,8 +1959,8 @@ var encryptCommand = defineCommand5({
|
|
|
1548
1959
|
args: SCOPE_ARGS,
|
|
1549
1960
|
run({ args }) {
|
|
1550
1961
|
return guard(async () => {
|
|
1551
|
-
const
|
|
1552
|
-
write(renderReseal(
|
|
1962
|
+
const result2 = await runEncrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1963
|
+
write(renderReseal(result2, "Encrypted"));
|
|
1553
1964
|
});
|
|
1554
1965
|
}
|
|
1555
1966
|
});
|
|
@@ -1558,24 +1969,129 @@ var decryptCommand = defineCommand5({
|
|
|
1558
1969
|
args: SCOPE_ARGS,
|
|
1559
1970
|
run({ args }) {
|
|
1560
1971
|
return guard(async () => {
|
|
1561
|
-
const
|
|
1562
|
-
write(renderReseal(
|
|
1972
|
+
const result2 = await runDecrypt({ cwd: process.cwd(), key: args.key, ...scopeOptions(args) });
|
|
1973
|
+
write(renderReseal(result2, "Decrypted"));
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
// src/commands/fill.ts
|
|
1979
|
+
import { createInterface } from "readline/promises";
|
|
1980
|
+
import { PenvError as PenvError8 } from "@penvhq/core";
|
|
1981
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1982
|
+
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
1983
|
+
async function runFill(options) {
|
|
1984
|
+
const validation = await runValidate({
|
|
1985
|
+
cwd: options.cwd,
|
|
1986
|
+
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
1987
|
+
});
|
|
1988
|
+
const environment = validation.environment;
|
|
1989
|
+
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
1990
|
+
if (blockers.length > 0) {
|
|
1991
|
+
const detail = blockers.map(
|
|
1992
|
+
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
1993
|
+
).join("\n");
|
|
1994
|
+
throw new PenvError8(
|
|
1995
|
+
"FILL_BLOCKED",
|
|
1996
|
+
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
1997
|
+
${detail}`,
|
|
1998
|
+
`Fix these \u2014 \`penv validate\` reports them \u2014 then run \`penv fill\`. If you have not written a schema yet, declare the required parameters in ${PENV_DIR}/env.ts.`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
const written = [];
|
|
2002
|
+
const skipped2 = [];
|
|
2003
|
+
const unreachable = [];
|
|
2004
|
+
for (const drift of validation.drift.declared) {
|
|
2005
|
+
if (drift.ref === void 0) {
|
|
2006
|
+
unreachable.push({ subject: drift.subject, remedy: drift.remedy });
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
const ref = drift.ref;
|
|
2010
|
+
const key = [...ref.namespace, ref.name].join("/");
|
|
2011
|
+
const value = await options.ask({ parameter: key, environment, secret: false });
|
|
2012
|
+
if (value === void 0 || value === "") {
|
|
2013
|
+
skipped2.push(drift.subject);
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const result2 = await runSet({ cwd: options.cwd, key, value, environment });
|
|
2017
|
+
written.push({
|
|
2018
|
+
parameter: drift.subject,
|
|
2019
|
+
location: result2.location,
|
|
2020
|
+
encrypted: result2.encrypted
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
return { environment, written, skipped: skipped2, unreachable };
|
|
2024
|
+
}
|
|
2025
|
+
function summaryLine(result2) {
|
|
2026
|
+
const filled = result2.written.length;
|
|
2027
|
+
if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
|
|
2028
|
+
return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
|
|
2029
|
+
}
|
|
2030
|
+
const parts = [`${filled} written`];
|
|
2031
|
+
if (result2.skipped.length > 0) {
|
|
2032
|
+
parts.push(`${result2.skipped.length} skipped`);
|
|
2033
|
+
}
|
|
2034
|
+
if (result2.unreachable.length > 0) {
|
|
2035
|
+
parts.push(`${result2.unreachable.length} unreachable`);
|
|
2036
|
+
}
|
|
2037
|
+
return `${parts.join(", ")} for environment ${result2.environment}`;
|
|
2038
|
+
}
|
|
2039
|
+
function renderFill(result2) {
|
|
2040
|
+
const rows = result2.written.map((entry) => ({
|
|
2041
|
+
glyph: CHECK,
|
|
2042
|
+
label: "Wrote",
|
|
2043
|
+
subject: `${PENV_DIR}/${entry.location}`,
|
|
2044
|
+
// Meta decided the seal, not the answer, so the line says so — as `set` does.
|
|
2045
|
+
...entry.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
|
|
2046
|
+
}));
|
|
2047
|
+
for (const entry of result2.unreachable) {
|
|
2048
|
+
rows.push({ glyph: WARN, label: "Unreachable", subject: entry.subject, detail: entry.remedy });
|
|
2049
|
+
}
|
|
2050
|
+
const lines = formatRows(rows);
|
|
2051
|
+
lines.push(summaryLine(result2));
|
|
2052
|
+
return lines;
|
|
2053
|
+
}
|
|
2054
|
+
var fillCommand = defineCommand6({
|
|
2055
|
+
meta: {
|
|
2056
|
+
name: "fill",
|
|
2057
|
+
description: "Prompt for each declared parameter the tree has no value for"
|
|
2058
|
+
},
|
|
2059
|
+
args: {
|
|
2060
|
+
env: { type: "string", description: "The environment to fill" }
|
|
2061
|
+
},
|
|
2062
|
+
run({ args }) {
|
|
2063
|
+
return guard(async () => {
|
|
2064
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2065
|
+
const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
|
|
2066
|
+
try {
|
|
2067
|
+
write(
|
|
2068
|
+
renderFill(
|
|
2069
|
+
await runFill({
|
|
2070
|
+
cwd: process.cwd(),
|
|
2071
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2072
|
+
ask
|
|
2073
|
+
})
|
|
2074
|
+
)
|
|
2075
|
+
);
|
|
2076
|
+
} finally {
|
|
2077
|
+
rl.close();
|
|
2078
|
+
}
|
|
1563
2079
|
});
|
|
1564
2080
|
}
|
|
1565
2081
|
});
|
|
1566
2082
|
|
|
1567
2083
|
// src/commands/generate.ts
|
|
1568
2084
|
import { writeFileSync } from "fs";
|
|
1569
|
-
import { isAbsolute, relative, resolve as
|
|
2085
|
+
import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
1570
2086
|
import {
|
|
1571
2087
|
checkNameCollisions as checkNameCollisions3,
|
|
1572
2088
|
effectiveMeta as effectiveMeta2,
|
|
1573
|
-
PenvError as
|
|
2089
|
+
PenvError as PenvError9,
|
|
1574
2090
|
requireValue as requireValue2,
|
|
1575
2091
|
serializeDotenv,
|
|
1576
2092
|
variableName as variableName4
|
|
1577
2093
|
} from "@penvhq/core";
|
|
1578
|
-
import { defineCommand as
|
|
2094
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1579
2095
|
var DEFAULT_OUTPUT = ".env";
|
|
1580
2096
|
function entriesFor(project, environment, allowDecrypt) {
|
|
1581
2097
|
const keys = keySourceFor(project, environment);
|
|
@@ -1594,7 +2110,7 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
1594
2110
|
const winner = resolution.winner;
|
|
1595
2111
|
if (winner?.file.encrypted === true) {
|
|
1596
2112
|
if (!allowDecrypt) {
|
|
1597
|
-
throw new
|
|
2113
|
+
throw new PenvError9(
|
|
1598
2114
|
"ENCRYPTED_VALUE_REFUSED",
|
|
1599
2115
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
1600
2116
|
`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.`
|
|
@@ -1625,7 +2141,7 @@ function runGenerate(options) {
|
|
|
1625
2141
|
const project = openProject(options.cwd);
|
|
1626
2142
|
const environment = targetEnvironment(project, options.environment);
|
|
1627
2143
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
1628
|
-
const file = options.out === void 0 ?
|
|
2144
|
+
const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
|
|
1629
2145
|
writeFileSync(file, serializeDotenv(entries), "utf8");
|
|
1630
2146
|
return { file, environment, entries: entries.length, decrypted };
|
|
1631
2147
|
}
|
|
@@ -1633,26 +2149,26 @@ function displayPath(cwd, file) {
|
|
|
1633
2149
|
const rel = relative(cwd, file);
|
|
1634
2150
|
return rel === "" || rel.startsWith("..") ? file : rel.split("\\").join("/");
|
|
1635
2151
|
}
|
|
1636
|
-
function renderGenerate(
|
|
2152
|
+
function renderGenerate(result2, cwd) {
|
|
1637
2153
|
const rows = [
|
|
1638
2154
|
{
|
|
1639
2155
|
glyph: CHECK,
|
|
1640
2156
|
label: "Generated",
|
|
1641
|
-
subject: displayPath(cwd,
|
|
1642
|
-
detail: `${
|
|
2157
|
+
subject: displayPath(cwd, result2.file),
|
|
2158
|
+
detail: `${result2.entries} variables for environment ${result2.environment}`
|
|
1643
2159
|
}
|
|
1644
2160
|
];
|
|
1645
|
-
if (
|
|
2161
|
+
if (result2.decrypted > 0) {
|
|
1646
2162
|
rows.push({
|
|
1647
2163
|
glyph: WARN,
|
|
1648
2164
|
label: "Decrypted",
|
|
1649
|
-
subject: `${
|
|
2165
|
+
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
1650
2166
|
detail: "written as plaintext into the artifact"
|
|
1651
2167
|
});
|
|
1652
2168
|
}
|
|
1653
2169
|
return formatRows(rows);
|
|
1654
2170
|
}
|
|
1655
|
-
var generateCommand =
|
|
2171
|
+
var generateCommand = defineCommand7({
|
|
1656
2172
|
meta: { name: "generate", description: "Write a standard .env artifact for deploy targets" },
|
|
1657
2173
|
args: {
|
|
1658
2174
|
env: { type: "string", description: "The environment to generate for" },
|
|
@@ -1665,20 +2181,20 @@ var generateCommand = defineCommand6({
|
|
|
1665
2181
|
run({ args }) {
|
|
1666
2182
|
return guard(async () => {
|
|
1667
2183
|
const cwd = process.cwd();
|
|
1668
|
-
const
|
|
2184
|
+
const result2 = runGenerate({
|
|
1669
2185
|
cwd,
|
|
1670
2186
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
1671
2187
|
...args.out === void 0 ? {} : { out: args.out },
|
|
1672
2188
|
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1673
2189
|
});
|
|
1674
|
-
write(renderGenerate(
|
|
2190
|
+
write(renderGenerate(result2, cwd));
|
|
1675
2191
|
});
|
|
1676
2192
|
}
|
|
1677
2193
|
});
|
|
1678
2194
|
|
|
1679
2195
|
// src/commands/get.ts
|
|
1680
|
-
import { PenvError as
|
|
1681
|
-
import { defineCommand as
|
|
2196
|
+
import { PenvError as PenvError10, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
|
|
2197
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1682
2198
|
async function runGet(options) {
|
|
1683
2199
|
const project = openProject(options.cwd);
|
|
1684
2200
|
const environment = targetEnvironment(project, options.environment);
|
|
@@ -1687,7 +2203,7 @@ async function runGet(options) {
|
|
|
1687
2203
|
const resolution = await resolveParameter(ref, environment, project.provider, keys);
|
|
1688
2204
|
const value = requireValue3(resolution, environment);
|
|
1689
2205
|
if (value === void 0) {
|
|
1690
|
-
throw new
|
|
2206
|
+
throw new PenvError10(
|
|
1691
2207
|
"PARAMETER_ABSENT",
|
|
1692
2208
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
1693
2209
|
`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.`
|
|
@@ -1737,7 +2253,7 @@ function renderExplain(explanation) {
|
|
|
1737
2253
|
...columns(rows).map((line) => ` ${line}`)
|
|
1738
2254
|
];
|
|
1739
2255
|
}
|
|
1740
|
-
var getCommand =
|
|
2256
|
+
var getCommand = defineCommand8({
|
|
1741
2257
|
meta: { name: "get", description: "Read a parameter" },
|
|
1742
2258
|
args: {
|
|
1743
2259
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -1762,17 +2278,17 @@ var getCommand = defineCommand7({
|
|
|
1762
2278
|
|
|
1763
2279
|
// src/commands/import.ts
|
|
1764
2280
|
import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1765
|
-
import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as
|
|
2281
|
+
import { basename, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve5 } from "path";
|
|
1766
2282
|
import {
|
|
1767
2283
|
accessPath as accessPath4,
|
|
1768
|
-
assertNever,
|
|
2284
|
+
assertNever as assertNever2,
|
|
1769
2285
|
checkNameCollisions as checkNameCollisions4,
|
|
1770
2286
|
FilenameGrammarError,
|
|
1771
2287
|
findConfigFile,
|
|
1772
2288
|
isReservedToken as isReservedToken3,
|
|
1773
2289
|
loadConfigFrom as loadConfigFrom2,
|
|
1774
2290
|
lookupEnvironment,
|
|
1775
|
-
PenvError as
|
|
2291
|
+
PenvError as PenvError12,
|
|
1776
2292
|
parseDotenv,
|
|
1777
2293
|
ReservedTokenError as ReservedTokenError3,
|
|
1778
2294
|
refFromVariable,
|
|
@@ -1781,7 +2297,7 @@ import {
|
|
|
1781
2297
|
UnknownEnvironmentError,
|
|
1782
2298
|
variableName as variableName5
|
|
1783
2299
|
} from "@penvhq/core";
|
|
1784
|
-
import { defineCommand as
|
|
2300
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
1785
2301
|
|
|
1786
2302
|
// src/detect.ts
|
|
1787
2303
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -1886,19 +2402,19 @@ function hasImportsBlock(cwd) {
|
|
|
1886
2402
|
|
|
1887
2403
|
// src/commands/init.ts
|
|
1888
2404
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1889
|
-
import { dirname as
|
|
1890
|
-
import { createInterface } from "readline/promises";
|
|
2405
|
+
import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
|
|
2406
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
1891
2407
|
import {
|
|
1892
2408
|
DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
|
|
1893
2409
|
isLegalEnvironmentName,
|
|
1894
2410
|
loadConfigFrom,
|
|
1895
|
-
PenvError as
|
|
2411
|
+
PenvError as PenvError11,
|
|
1896
2412
|
RESERVED_TOKENS,
|
|
1897
2413
|
schemaFileOf as schemaFileOf2,
|
|
1898
2414
|
schemaInsideTree,
|
|
1899
2415
|
validateSchemaFile
|
|
1900
2416
|
} from "@penvhq/core";
|
|
1901
|
-
import { defineCommand as
|
|
2417
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
1902
2418
|
var CONFIG_FILE = "penv.config.ts";
|
|
1903
2419
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
1904
2420
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -1936,7 +2452,7 @@ function suggestEnvironments(root) {
|
|
|
1936
2452
|
return [...found].sort();
|
|
1937
2453
|
}
|
|
1938
2454
|
function emptyFlag(flag) {
|
|
1939
|
-
return new
|
|
2455
|
+
return new PenvError11(
|
|
1940
2456
|
"INIT_FLAG_EMPTY",
|
|
1941
2457
|
`\`--${flag}\` was given without a value`,
|
|
1942
2458
|
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."
|
|
@@ -1999,7 +2515,7 @@ function planInit(root, flags = {}) {
|
|
|
1999
2515
|
throw emptyFlag("alias");
|
|
2000
2516
|
}
|
|
2001
2517
|
if (!ALIAS_NAME.test(alias)) {
|
|
2002
|
-
throw new
|
|
2518
|
+
throw new PenvError11(
|
|
2003
2519
|
"INIT_ALIAS_INVALID",
|
|
2004
2520
|
`\`${alias}\` is not an alias penv can write`,
|
|
2005
2521
|
`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.`
|
|
@@ -2286,7 +2802,7 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
2286
2802
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
2287
2803
|
}
|
|
2288
2804
|
function shapeError(what, target, alias) {
|
|
2289
|
-
return new
|
|
2805
|
+
return new PenvError11(
|
|
2290
2806
|
"TSCONFIG_SHAPE",
|
|
2291
2807
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
2292
2808
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
@@ -2356,7 +2872,7 @@ function renderTsconfig(target, alias) {
|
|
|
2356
2872
|
`;
|
|
2357
2873
|
}
|
|
2358
2874
|
function ensurePenvDir(root) {
|
|
2359
|
-
const dir =
|
|
2875
|
+
const dir = resolve4(root, PENV_DIR2);
|
|
2360
2876
|
if (existsSync2(dir)) {
|
|
2361
2877
|
return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
|
|
2362
2878
|
}
|
|
@@ -2373,7 +2889,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2373
2889
|
note: "(yours \u2014 penv never regenerates it)"
|
|
2374
2890
|
};
|
|
2375
2891
|
}
|
|
2376
|
-
mkdirSync(
|
|
2892
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
2377
2893
|
writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
|
|
2378
2894
|
return {
|
|
2379
2895
|
target: "schema",
|
|
@@ -2461,26 +2977,26 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
2461
2977
|
];
|
|
2462
2978
|
}
|
|
2463
2979
|
function runInit(options) {
|
|
2464
|
-
const root =
|
|
2980
|
+
const root = resolve4(options.cwd);
|
|
2465
2981
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
2466
2982
|
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
2467
2983
|
}
|
|
2468
|
-
function renderInit(
|
|
2469
|
-
const steps =
|
|
2984
|
+
function renderInit(result2) {
|
|
2985
|
+
const steps = result2.steps.map((step) => {
|
|
2470
2986
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
2471
2987
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
2472
2988
|
});
|
|
2473
2989
|
return [
|
|
2474
2990
|
...formatSteps(steps),
|
|
2475
2991
|
"",
|
|
2476
|
-
`Done. Declare your parameters in ${
|
|
2477
|
-
...
|
|
2992
|
+
`Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
|
|
2993
|
+
...result2.decisions.environments.length === 0 ? [
|
|
2478
2994
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
2479
2995
|
] : []
|
|
2480
2996
|
];
|
|
2481
2997
|
}
|
|
2482
2998
|
async function askOnTty(plan2) {
|
|
2483
|
-
const rl =
|
|
2999
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2484
3000
|
try {
|
|
2485
3001
|
return await promptForDecisions(plan2, {
|
|
2486
3002
|
ask: (question) => rl.question(question),
|
|
@@ -2491,7 +3007,7 @@ async function askOnTty(plan2) {
|
|
|
2491
3007
|
rl.close();
|
|
2492
3008
|
}
|
|
2493
3009
|
}
|
|
2494
|
-
var initCommand =
|
|
3010
|
+
var initCommand = defineCommand9({
|
|
2495
3011
|
meta: { name: "init", description: "Initialize a project (.penv/, env.ts, config, @env alias)" },
|
|
2496
3012
|
args: {
|
|
2497
3013
|
yes: {
|
|
@@ -2513,7 +3029,7 @@ var initCommand = defineCommand8({
|
|
|
2513
3029
|
},
|
|
2514
3030
|
run({ args }) {
|
|
2515
3031
|
return guard(async () => {
|
|
2516
|
-
const root =
|
|
3032
|
+
const root = resolve4(process.cwd());
|
|
2517
3033
|
const environments = environmentsFromFlag(args.env);
|
|
2518
3034
|
const plan2 = planInit(root, {
|
|
2519
3035
|
...args.schema === void 0 ? {} : { schema: args.schema },
|
|
@@ -2565,7 +3081,7 @@ function assertImportable(ref, variable) {
|
|
|
2565
3081
|
if (!ref.name.includes(".")) {
|
|
2566
3082
|
return;
|
|
2567
3083
|
}
|
|
2568
|
-
throw new
|
|
3084
|
+
throw new PenvError12(
|
|
2569
3085
|
"IMPORT_UNPARSEABLE_NAME",
|
|
2570
3086
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
2571
3087
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
@@ -2584,7 +3100,7 @@ function assertRoundTrips(ref, variable, config) {
|
|
|
2584
3100
|
return;
|
|
2585
3101
|
}
|
|
2586
3102
|
const generated = variableName5(ref, config);
|
|
2587
|
-
throw new
|
|
3103
|
+
throw new PenvError12(
|
|
2588
3104
|
"IMPORT_LOSSY_NAME",
|
|
2589
3105
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
2590
3106
|
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`names\` block of penv.config.ts \u2014 \`names: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
|
|
@@ -2647,7 +3163,7 @@ function environmentOf(scope) {
|
|
|
2647
3163
|
case "local":
|
|
2648
3164
|
return void 0;
|
|
2649
3165
|
default:
|
|
2650
|
-
return
|
|
3166
|
+
return assertNever2(scope, "scope");
|
|
2651
3167
|
}
|
|
2652
3168
|
}
|
|
2653
3169
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -2660,7 +3176,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
2660
3176
|
case "environment-local":
|
|
2661
3177
|
return scope;
|
|
2662
3178
|
default:
|
|
2663
|
-
return
|
|
3179
|
+
return assertNever2(scope, "scope");
|
|
2664
3180
|
}
|
|
2665
3181
|
}
|
|
2666
3182
|
function explicitEnvironment(options, source, config) {
|
|
@@ -2668,7 +3184,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
2668
3184
|
if (value.length > 0) {
|
|
2669
3185
|
return value;
|
|
2670
3186
|
}
|
|
2671
|
-
throw new
|
|
3187
|
+
throw new PenvError12(
|
|
2672
3188
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
2673
3189
|
`\`--env\` for the import of ${source} names no environment`,
|
|
2674
3190
|
`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.`
|
|
@@ -2678,7 +3194,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
2678
3194
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
2679
3195
|
return;
|
|
2680
3196
|
}
|
|
2681
|
-
throw new
|
|
3197
|
+
throw new PenvError12(
|
|
2682
3198
|
"IMPORT_ENV_CONFLICT",
|
|
2683
3199
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
2684
3200
|
`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.`
|
|
@@ -2719,10 +3235,10 @@ function configInEffect(cwd, environment) {
|
|
|
2719
3235
|
return { config: openProject(cwd).config, decisions };
|
|
2720
3236
|
}
|
|
2721
3237
|
function importDotenv(options) {
|
|
2722
|
-
const cwd =
|
|
2723
|
-
const file = isAbsolute2(options.file) ? options.file :
|
|
3238
|
+
const cwd = resolve5(options.cwd);
|
|
3239
|
+
const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
|
|
2724
3240
|
if (!existsSync3(file)) {
|
|
2725
|
-
throw new
|
|
3241
|
+
throw new PenvError12(
|
|
2726
3242
|
"IMPORT_FILE_MISSING",
|
|
2727
3243
|
`There is no file at ${file} to import`,
|
|
2728
3244
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
@@ -2803,19 +3319,19 @@ function skippedValidationStep(environments) {
|
|
|
2803
3319
|
note: `(no environment set \u2014 run \`penv validate --env ${example}\`)`
|
|
2804
3320
|
};
|
|
2805
3321
|
}
|
|
2806
|
-
function renderImport(
|
|
2807
|
-
const steps = [{ glyph: CHECK, text: `Found ${
|
|
2808
|
-
if (
|
|
2809
|
-
const plural =
|
|
3322
|
+
function renderImport(result2, validation) {
|
|
3323
|
+
const steps = [{ glyph: CHECK, text: `Found ${result2.variables} variables` }];
|
|
3324
|
+
if (result2.orphanComments > 0) {
|
|
3325
|
+
const plural = result2.orphanComments === 1 ? "comment" : "comments";
|
|
2810
3326
|
steps.push({
|
|
2811
3327
|
glyph: WARN,
|
|
2812
|
-
text: `Dropped ${
|
|
3328
|
+
text: `Dropped ${result2.orphanComments} orphan ${plural}`,
|
|
2813
3329
|
note: "attached to no variable, so nothing to describe"
|
|
2814
3330
|
});
|
|
2815
3331
|
}
|
|
2816
|
-
for (const step of
|
|
3332
|
+
for (const step of result2.steps) {
|
|
2817
3333
|
if (step.target === "schema" && step.action === "kept") {
|
|
2818
|
-
steps.push(keptSchemaStep(step,
|
|
3334
|
+
steps.push(keptSchemaStep(step, result2.variables));
|
|
2819
3335
|
continue;
|
|
2820
3336
|
}
|
|
2821
3337
|
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
@@ -2823,10 +3339,10 @@ function renderImport(result, validation) {
|
|
|
2823
3339
|
step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note }
|
|
2824
3340
|
);
|
|
2825
3341
|
}
|
|
2826
|
-
steps.push({ glyph: CHECK, text: `Created ${displayPath2(
|
|
3342
|
+
steps.push({ glyph: CHECK, text: `Created ${displayPath2(result2.root, result2.backup)}` });
|
|
2827
3343
|
const lines = formatSteps(steps);
|
|
2828
3344
|
if (validation === void 0) {
|
|
2829
|
-
lines.push(...formatSteps([skippedValidationStep(
|
|
3345
|
+
lines.push(...formatSteps([skippedValidationStep(result2.environments)]));
|
|
2830
3346
|
} else if (validation.ok) {
|
|
2831
3347
|
lines.push(...formatSteps([{ glyph: CHECK, text: "Validated configuration" }]));
|
|
2832
3348
|
} else {
|
|
@@ -2835,7 +3351,7 @@ function renderImport(result, validation) {
|
|
|
2835
3351
|
lines.push("", "Done. .penv/ is now your source of truth.");
|
|
2836
3352
|
return lines;
|
|
2837
3353
|
}
|
|
2838
|
-
var importCommand =
|
|
3354
|
+
var importCommand = defineCommand10({
|
|
2839
3355
|
meta: {
|
|
2840
3356
|
name: "import",
|
|
2841
3357
|
description: "Import an existing dotenv file; it becomes the source of truth"
|
|
@@ -2867,15 +3383,15 @@ var importCommand = defineCommand9({
|
|
|
2867
3383
|
|
|
2868
3384
|
// src/commands/key.ts
|
|
2869
3385
|
import { randomBytes } from "crypto";
|
|
2870
|
-
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as
|
|
2871
|
-
import { defineCommand as
|
|
3386
|
+
import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError13 } from "@penvhq/core";
|
|
3387
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
2872
3388
|
|
|
2873
3389
|
// src/keychain.ts
|
|
2874
|
-
import { createRequire } from "module";
|
|
3390
|
+
import { createRequire as createRequire2 } from "module";
|
|
2875
3391
|
var cached;
|
|
2876
3392
|
function entryConstructor() {
|
|
2877
3393
|
if (cached === void 0) {
|
|
2878
|
-
const require2 =
|
|
3394
|
+
const require2 = createRequire2(import.meta.url);
|
|
2879
3395
|
cached = require2("@napi-rs/keyring").Entry;
|
|
2880
3396
|
}
|
|
2881
3397
|
return cached;
|
|
@@ -2900,7 +3416,7 @@ function runKeyCreate(options) {
|
|
|
2900
3416
|
const environment = targetEnvironment(project, options.environment);
|
|
2901
3417
|
const declared = project.config.keys?.[environment];
|
|
2902
3418
|
if (declared === void 0) {
|
|
2903
|
-
throw new
|
|
3419
|
+
throw new PenvError13(
|
|
2904
3420
|
"KEY_SOURCE_UNDECLARED",
|
|
2905
3421
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
2906
3422
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
@@ -2914,14 +3430,14 @@ function runKeyCreate(options) {
|
|
|
2914
3430
|
try {
|
|
2915
3431
|
existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
|
|
2916
3432
|
} catch (cause) {
|
|
2917
|
-
throw new
|
|
3433
|
+
throw new PenvError13(
|
|
2918
3434
|
"KEYCHAIN_UNAVAILABLE",
|
|
2919
3435
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
2920
3436
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2921
3437
|
);
|
|
2922
3438
|
}
|
|
2923
3439
|
if (existing !== null) {
|
|
2924
|
-
throw new
|
|
3440
|
+
throw new PenvError13(
|
|
2925
3441
|
"KEY_EXISTS",
|
|
2926
3442
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
2927
3443
|
"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."
|
|
@@ -2939,29 +3455,29 @@ function runKeyCreate(options) {
|
|
|
2939
3455
|
key
|
|
2940
3456
|
};
|
|
2941
3457
|
}
|
|
2942
|
-
function renderKeyCreate(
|
|
2943
|
-
if (
|
|
3458
|
+
function renderKeyCreate(result2) {
|
|
3459
|
+
if (result2.source === "keychain") {
|
|
2944
3460
|
return [
|
|
2945
|
-
`A new key for environment ${
|
|
3461
|
+
`A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
2946
3462
|
"",
|
|
2947
3463
|
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
|
|
2948
3464
|
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
2949
3465
|
];
|
|
2950
3466
|
}
|
|
2951
3467
|
return [
|
|
2952
|
-
`A new key for environment ${
|
|
3468
|
+
`A new key for environment ${result2.environment}. penv did not store it.`,
|
|
2953
3469
|
"",
|
|
2954
|
-
` ${
|
|
3470
|
+
` ${result2.variable}=${result2.key}`,
|
|
2955
3471
|
"",
|
|
2956
3472
|
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
|
|
2957
3473
|
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
|
|
2958
3474
|
"without it, and penv keeps no copy to fall back on."
|
|
2959
3475
|
];
|
|
2960
3476
|
}
|
|
2961
|
-
var keyCommand =
|
|
3477
|
+
var keyCommand = defineCommand11({
|
|
2962
3478
|
meta: { name: "key", description: "Work with encryption keys" },
|
|
2963
3479
|
subCommands: {
|
|
2964
|
-
create:
|
|
3480
|
+
create: defineCommand11({
|
|
2965
3481
|
meta: { name: "create", description: "Generate a key for an environment" },
|
|
2966
3482
|
args: {
|
|
2967
3483
|
env: { type: "string", description: "The environment the key is for" },
|
|
@@ -2988,8 +3504,8 @@ var keyCommand = defineCommand10({
|
|
|
2988
3504
|
});
|
|
2989
3505
|
|
|
2990
3506
|
// src/commands/list.ts
|
|
2991
|
-
import { assertNever as
|
|
2992
|
-
import { defineCommand as
|
|
3507
|
+
import { assertNever as assertNever3, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
|
|
3508
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
2993
3509
|
function scopeLabel2(scope) {
|
|
2994
3510
|
switch (scope.kind) {
|
|
2995
3511
|
case "environment":
|
|
@@ -3001,7 +3517,7 @@ function scopeLabel2(scope) {
|
|
|
3001
3517
|
case "unscoped":
|
|
3002
3518
|
return "default";
|
|
3003
3519
|
default:
|
|
3004
|
-
return
|
|
3520
|
+
return assertNever3(scope, "scope");
|
|
3005
3521
|
}
|
|
3006
3522
|
}
|
|
3007
3523
|
async function runList(options) {
|
|
@@ -3023,13 +3539,13 @@ async function runList(options) {
|
|
|
3023
3539
|
}
|
|
3024
3540
|
return { environment, parameters };
|
|
3025
3541
|
}
|
|
3026
|
-
function renderList(
|
|
3027
|
-
if (
|
|
3028
|
-
return [`No parameters in ${PENV_DIR}/ for environment ${
|
|
3542
|
+
function renderList(result2) {
|
|
3543
|
+
if (result2.parameters.length === 0) {
|
|
3544
|
+
return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
|
|
3029
3545
|
}
|
|
3030
|
-
return columns(
|
|
3546
|
+
return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
|
|
3031
3547
|
}
|
|
3032
|
-
var listCommand =
|
|
3548
|
+
var listCommand = defineCommand12({
|
|
3033
3549
|
meta: { name: "list", description: "List parameters" },
|
|
3034
3550
|
args: {
|
|
3035
3551
|
env: { type: "string", description: "The environment to resolve against" },
|
|
@@ -3037,11 +3553,11 @@ var listCommand = defineCommand11({
|
|
|
3037
3553
|
},
|
|
3038
3554
|
run({ args }) {
|
|
3039
3555
|
return guard(async () => {
|
|
3040
|
-
const
|
|
3556
|
+
const result2 = await runList({
|
|
3041
3557
|
cwd: process.cwd(),
|
|
3042
3558
|
...args.env === void 0 ? {} : { environment: args.env }
|
|
3043
3559
|
});
|
|
3044
|
-
write(args.json === true ? [JSON.stringify(
|
|
3560
|
+
write(args.json === true ? [JSON.stringify(result2, null, 2)] : renderList(result2));
|
|
3045
3561
|
});
|
|
3046
3562
|
}
|
|
3047
3563
|
});
|
|
@@ -3050,13 +3566,13 @@ var listCommand = defineCommand11({
|
|
|
3050
3566
|
import {
|
|
3051
3567
|
accessPath as accessPath5,
|
|
3052
3568
|
formatMetaFile,
|
|
3053
|
-
formatValueFile as
|
|
3054
|
-
openValue as
|
|
3055
|
-
PenvError as
|
|
3569
|
+
formatValueFile as formatValueFile5,
|
|
3570
|
+
openValue as openValue4,
|
|
3571
|
+
PenvError as PenvError14,
|
|
3056
3572
|
parameterId as parameterId5,
|
|
3057
3573
|
sealValue as sealValue3
|
|
3058
3574
|
} from "@penvhq/core";
|
|
3059
|
-
import { defineCommand as
|
|
3575
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
3060
3576
|
function environmentOf2(file) {
|
|
3061
3577
|
const scope = file.scope;
|
|
3062
3578
|
return scope.kind === "environment" || scope.kind === "environment-local" ? scope.environment : void 0;
|
|
@@ -3071,18 +3587,18 @@ async function planFile(project, source, target, parameter) {
|
|
|
3071
3587
|
}
|
|
3072
3588
|
const environment = environmentOf2(source);
|
|
3073
3589
|
if (environment === void 0) {
|
|
3074
|
-
throw new
|
|
3590
|
+
throw new PenvError14(
|
|
3075
3591
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3076
|
-
`${PENV_DIR}/${
|
|
3592
|
+
`${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3077
3593
|
"Keys are declared per environment in the `keys` block of penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
|
|
3078
3594
|
);
|
|
3079
3595
|
}
|
|
3080
3596
|
const keys = keySourceFor(project, environment);
|
|
3081
|
-
const opened =
|
|
3597
|
+
const opened = openValue4(source, stored, keys);
|
|
3082
3598
|
if (opened.kind === "failed") {
|
|
3083
|
-
throw new
|
|
3599
|
+
throw new PenvError14(
|
|
3084
3600
|
"VALUE_UNDECRYPTABLE",
|
|
3085
|
-
`${PENV_DIR}/${
|
|
3601
|
+
`${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3086
3602
|
"A sealed value is bound to the file it lives in, so moving it means opening it and sealing it again. Make the key available and run this again. Nothing has been moved."
|
|
3087
3603
|
);
|
|
3088
3604
|
}
|
|
@@ -3100,9 +3616,10 @@ function filesOf(all, ref) {
|
|
|
3100
3616
|
async function runMove(options) {
|
|
3101
3617
|
const project = openProject(options.cwd);
|
|
3102
3618
|
const from = refFromKey(options.from, project.config);
|
|
3619
|
+
assertWritableKey(options.to);
|
|
3103
3620
|
const to = refFromKey(options.to, project.config);
|
|
3104
3621
|
if (parameterId5(from) === parameterId5(to)) {
|
|
3105
|
-
throw new
|
|
3622
|
+
throw new PenvError14(
|
|
3106
3623
|
"PARAMETER_UNCHANGED",
|
|
3107
3624
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3108
3625
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3112,7 +3629,7 @@ async function runMove(options) {
|
|
|
3112
3629
|
const sources = filesOf(all, from);
|
|
3113
3630
|
const meta = await project.provider.readMeta(from);
|
|
3114
3631
|
if (sources.length === 0 && meta === void 0) {
|
|
3115
|
-
throw new
|
|
3632
|
+
throw new PenvError14(
|
|
3116
3633
|
"PARAMETER_ABSENT",
|
|
3117
3634
|
`Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
|
|
3118
3635
|
`\`penv list\` shows every parameter penv holds.`
|
|
@@ -3120,7 +3637,7 @@ async function runMove(options) {
|
|
|
3120
3637
|
}
|
|
3121
3638
|
const occupied2 = filesOf(all, to);
|
|
3122
3639
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3123
|
-
throw new
|
|
3640
|
+
throw new PenvError14(
|
|
3124
3641
|
"PARAMETER_EXISTS",
|
|
3125
3642
|
`Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
|
|
3126
3643
|
`Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
@@ -3150,8 +3667,8 @@ async function runMove(options) {
|
|
|
3150
3667
|
from: parameterId5(from),
|
|
3151
3668
|
to: parameterId5(to),
|
|
3152
3669
|
files: planned.map((file) => ({
|
|
3153
|
-
from:
|
|
3154
|
-
to:
|
|
3670
|
+
from: formatValueFile5(file.source),
|
|
3671
|
+
to: formatValueFile5(file.target),
|
|
3155
3672
|
resealed: file.resealed
|
|
3156
3673
|
})),
|
|
3157
3674
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
@@ -3161,25 +3678,25 @@ async function runMove(options) {
|
|
|
3161
3678
|
schema: { was: accessPath5(from).join("."), now: accessPath5(to).join(".") }
|
|
3162
3679
|
};
|
|
3163
3680
|
}
|
|
3164
|
-
function renderMove(
|
|
3165
|
-
const rows =
|
|
3681
|
+
function renderMove(result2) {
|
|
3682
|
+
const rows = result2.files.map((file) => ({
|
|
3166
3683
|
glyph: CHECK,
|
|
3167
3684
|
label: "Moved",
|
|
3168
3685
|
subject: `${PENV_DIR}/${file.to}`,
|
|
3169
3686
|
...file.resealed ? { detail: "re-sealed for its new address" } : {}
|
|
3170
3687
|
}));
|
|
3171
|
-
if (
|
|
3172
|
-
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${
|
|
3688
|
+
if (result2.meta !== void 0) {
|
|
3689
|
+
rows.push({ glyph: CHECK, label: "Moved", subject: `${PENV_DIR}/${result2.meta}` });
|
|
3173
3690
|
}
|
|
3174
3691
|
const lines = formatRows(rows);
|
|
3175
3692
|
lines.push(
|
|
3176
3693
|
"",
|
|
3177
|
-
` .penv/env.ts still declares \`${
|
|
3694
|
+
` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
|
|
3178
3695
|
" or `penv validate` will report the value as unused and the declaration as unset."
|
|
3179
3696
|
);
|
|
3180
3697
|
return lines;
|
|
3181
3698
|
}
|
|
3182
|
-
var mvCommand =
|
|
3699
|
+
var mvCommand = defineCommand13({
|
|
3183
3700
|
meta: { name: "mv", description: "Rename a parameter, every scope and its meta at once" },
|
|
3184
3701
|
args: {
|
|
3185
3702
|
from: {
|
|
@@ -3200,9 +3717,86 @@ var mvCommand = defineCommand12({
|
|
|
3200
3717
|
}
|
|
3201
3718
|
});
|
|
3202
3719
|
|
|
3720
|
+
// src/commands/pull.ts
|
|
3721
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
3722
|
+
async function runPull(options) {
|
|
3723
|
+
const project = openProject(options.cwd);
|
|
3724
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3725
|
+
const source = await sourceProviderFor(project, environment);
|
|
3726
|
+
if (source.type === LOCAL_TREE_TYPE) {
|
|
3727
|
+
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3728
|
+
}
|
|
3729
|
+
const tree = localTree(project);
|
|
3730
|
+
const files = await source.list();
|
|
3731
|
+
let values = 0;
|
|
3732
|
+
for (const file of files) {
|
|
3733
|
+
const value = await source.read(file);
|
|
3734
|
+
if (value === void 0) {
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
tree.writeSync(file, value);
|
|
3738
|
+
values += 1;
|
|
3739
|
+
}
|
|
3740
|
+
const refs = refsFrom(files);
|
|
3741
|
+
let meta = 0;
|
|
3742
|
+
for (const ref of refs) {
|
|
3743
|
+
const block = await source.readMeta(ref);
|
|
3744
|
+
if (block === void 0) {
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
tree.writeMetaSync(ref, block);
|
|
3748
|
+
meta += 1;
|
|
3749
|
+
}
|
|
3750
|
+
return { environment, source: source.type, localSource: false, values, meta, refs: refs.length };
|
|
3751
|
+
}
|
|
3752
|
+
function renderPull(result2) {
|
|
3753
|
+
if (result2.localSource) {
|
|
3754
|
+
return formatRows([
|
|
3755
|
+
{
|
|
3756
|
+
glyph: CHECK,
|
|
3757
|
+
label: "Nothing to pull",
|
|
3758
|
+
subject: `environment ${result2.environment} has no separate source of truth`,
|
|
3759
|
+
detail: "its values live in the local .penv tree already"
|
|
3760
|
+
}
|
|
3761
|
+
]);
|
|
3762
|
+
}
|
|
3763
|
+
return formatRows([
|
|
3764
|
+
{
|
|
3765
|
+
glyph: CHECK,
|
|
3766
|
+
label: "Pulled",
|
|
3767
|
+
subject: `${result2.values} ${result2.values === 1 ? "value" : "values"}`,
|
|
3768
|
+
detail: `from the ${result2.source} provider for environment ${result2.environment}`
|
|
3769
|
+
},
|
|
3770
|
+
{
|
|
3771
|
+
glyph: CHECK,
|
|
3772
|
+
label: "Parameters",
|
|
3773
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter" : "parameters"}, ${result2.meta} with meta`,
|
|
3774
|
+
detail: "written into the local .penv tree"
|
|
3775
|
+
}
|
|
3776
|
+
]);
|
|
3777
|
+
}
|
|
3778
|
+
var pullCommand = defineCommand14({
|
|
3779
|
+
meta: {
|
|
3780
|
+
name: "pull",
|
|
3781
|
+
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3782
|
+
},
|
|
3783
|
+
args: {
|
|
3784
|
+
env: { type: "string", description: "The environment to pull" }
|
|
3785
|
+
},
|
|
3786
|
+
run({ args }) {
|
|
3787
|
+
return guard(async () => {
|
|
3788
|
+
const result2 = await runPull({
|
|
3789
|
+
cwd: process.cwd(),
|
|
3790
|
+
...args.env === void 0 ? {} : { environment: args.env }
|
|
3791
|
+
});
|
|
3792
|
+
write(renderPull(result2));
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
|
|
3203
3797
|
// src/commands/remove.ts
|
|
3204
|
-
import { formatValueFile as
|
|
3205
|
-
import { defineCommand as
|
|
3798
|
+
import { formatValueFile as formatValueFile6 } from "@penvhq/core";
|
|
3799
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
3206
3800
|
async function runRemove(options) {
|
|
3207
3801
|
const project = openProject(options.cwd);
|
|
3208
3802
|
const ref = refFromKey(options.key);
|
|
@@ -3219,17 +3813,17 @@ async function runRemove(options) {
|
|
|
3219
3813
|
continue;
|
|
3220
3814
|
}
|
|
3221
3815
|
await project.provider.remove(file);
|
|
3222
|
-
removed.push(
|
|
3816
|
+
removed.push(formatValueFile6(file));
|
|
3223
3817
|
}
|
|
3224
3818
|
return {
|
|
3225
3819
|
parameter: options.key,
|
|
3226
3820
|
removed,
|
|
3227
|
-
considered: files.map((file) =>
|
|
3821
|
+
considered: files.map((file) => formatValueFile6(file))
|
|
3228
3822
|
};
|
|
3229
3823
|
}
|
|
3230
|
-
function renderRemove(
|
|
3231
|
-
if (
|
|
3232
|
-
const first =
|
|
3824
|
+
function renderRemove(result2) {
|
|
3825
|
+
if (result2.removed.length === 0) {
|
|
3826
|
+
const first = result2.considered[0] ?? result2.parameter;
|
|
3233
3827
|
return formatRows([
|
|
3234
3828
|
{
|
|
3235
3829
|
glyph: WARN,
|
|
@@ -3239,14 +3833,14 @@ function renderRemove(result) {
|
|
|
3239
3833
|
}
|
|
3240
3834
|
]);
|
|
3241
3835
|
}
|
|
3242
|
-
const rows =
|
|
3836
|
+
const rows = result2.removed.map((location) => ({
|
|
3243
3837
|
glyph: CHECK,
|
|
3244
3838
|
label: "Removed",
|
|
3245
3839
|
subject: `${PENV_DIR}/${location}`
|
|
3246
3840
|
}));
|
|
3247
3841
|
return formatRows(rows);
|
|
3248
3842
|
}
|
|
3249
|
-
var removeCommand =
|
|
3843
|
+
var removeCommand = defineCommand15({
|
|
3250
3844
|
meta: { name: "remove", description: "Delete a parameter" },
|
|
3251
3845
|
args: {
|
|
3252
3846
|
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
@@ -3272,11 +3866,197 @@ var removeCommand = defineCommand13({
|
|
|
3272
3866
|
}
|
|
3273
3867
|
});
|
|
3274
3868
|
|
|
3869
|
+
// src/commands/rotate.ts
|
|
3870
|
+
import {
|
|
3871
|
+
beginRotation,
|
|
3872
|
+
completeRotation,
|
|
3873
|
+
PenvError as PenvError15,
|
|
3874
|
+
retainsPrevious,
|
|
3875
|
+
rotationOf as rotationOf2
|
|
3876
|
+
} from "@penvhq/core";
|
|
3877
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
3878
|
+
function rotatingFile(ref, environment) {
|
|
3879
|
+
return {
|
|
3880
|
+
namespace: ref.namespace,
|
|
3881
|
+
name: ref.name,
|
|
3882
|
+
scope: { kind: "environment", environment },
|
|
3883
|
+
encrypted: false
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
3887
|
+
if (provider.type === LOCAL_TREE_TYPE) {
|
|
3888
|
+
await sealAwareWrite({
|
|
3889
|
+
project,
|
|
3890
|
+
provider,
|
|
3891
|
+
ref,
|
|
3892
|
+
scope: { kind: "environment", environment },
|
|
3893
|
+
value,
|
|
3894
|
+
environment
|
|
3895
|
+
});
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
await provider.write(rotatingFile(ref, environment), value);
|
|
3899
|
+
}
|
|
3900
|
+
function requireNewValue(value, phase, key) {
|
|
3901
|
+
if (value === void 0) {
|
|
3902
|
+
throw new PenvError15(
|
|
3903
|
+
"ROTATION_NO_VALUE",
|
|
3904
|
+
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3905
|
+
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
return value;
|
|
3909
|
+
}
|
|
3910
|
+
function requireRetaining(provider, environment) {
|
|
3911
|
+
if (!retainsPrevious(provider)) {
|
|
3912
|
+
throw new PenvError15(
|
|
3913
|
+
"ROTATION_NOT_RETAINING",
|
|
3914
|
+
`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`,
|
|
3915
|
+
"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."
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
return provider;
|
|
3919
|
+
}
|
|
3920
|
+
async function runRotate(options) {
|
|
3921
|
+
const project = openProject(options.cwd);
|
|
3922
|
+
const environment = targetEnvironment(project, options.environment);
|
|
3923
|
+
const ref = refFromKey(options.key, project.config);
|
|
3924
|
+
const provider = await sourceProviderFor(project, environment);
|
|
3925
|
+
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3926
|
+
const before = await provider.readMeta(ref);
|
|
3927
|
+
const { mechanism } = rotationOf2(before, environment);
|
|
3928
|
+
if (mechanism === void 0) {
|
|
3929
|
+
throw new PenvError15(
|
|
3930
|
+
"ROTATION_NO_MECHANISM",
|
|
3931
|
+
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3932
|
+
'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.'
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
const begin = options.begin === true;
|
|
3936
|
+
const complete = options.complete === true;
|
|
3937
|
+
if (mechanism === "atomic-cutover") {
|
|
3938
|
+
if (begin || complete) {
|
|
3939
|
+
throw new PenvError15(
|
|
3940
|
+
"ROTATION_MECHANISM_MISMATCH",
|
|
3941
|
+
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3942
|
+
"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."
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
const value = requireNewValue(options.value, "cutover", options.key);
|
|
3946
|
+
await writeRotatedValue(project, provider, ref, environment, value);
|
|
3947
|
+
const after2 = completeRotation(before, environment, nowIso);
|
|
3948
|
+
await provider.writeMeta(ref, after2);
|
|
3949
|
+
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
3950
|
+
}
|
|
3951
|
+
const retaining = requireRetaining(provider, environment);
|
|
3952
|
+
if (begin === complete) {
|
|
3953
|
+
throw new PenvError15(
|
|
3954
|
+
"ROTATION_PHASE_REQUIRED",
|
|
3955
|
+
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3956
|
+
"`--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."
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3959
|
+
if (begin) {
|
|
3960
|
+
const value = requireNewValue(options.value, "begin", options.key);
|
|
3961
|
+
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
3962
|
+
const after2 = beginRotation(before, environment, nowIso);
|
|
3963
|
+
await retaining.writeMeta(ref, after2);
|
|
3964
|
+
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
3965
|
+
}
|
|
3966
|
+
const after = completeRotation(before, environment, nowIso);
|
|
3967
|
+
await retaining.writeMeta(ref, after);
|
|
3968
|
+
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
3969
|
+
}
|
|
3970
|
+
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
3971
|
+
const { state, rotatingSince, lastRotated } = rotationOf2(after, environment);
|
|
3972
|
+
return {
|
|
3973
|
+
parameter: [...ref.namespace, ref.name].join("/"),
|
|
3974
|
+
environment,
|
|
3975
|
+
mechanism,
|
|
3976
|
+
phase,
|
|
3977
|
+
source,
|
|
3978
|
+
wroteValue,
|
|
3979
|
+
state,
|
|
3980
|
+
rotatingSince,
|
|
3981
|
+
lastRotated
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
function renderRotate(result2) {
|
|
3985
|
+
if (result2.phase === "begin") {
|
|
3986
|
+
return formatRows([
|
|
3987
|
+
{
|
|
3988
|
+
glyph: CHECK,
|
|
3989
|
+
label: "Rotating",
|
|
3990
|
+
subject: result2.parameter,
|
|
3991
|
+
detail: `dual-valid window open for environment ${result2.environment} (since ${result2.rotatingSince})`
|
|
3992
|
+
},
|
|
3993
|
+
{
|
|
3994
|
+
glyph: CHECK,
|
|
3995
|
+
label: "Previous",
|
|
3996
|
+
subject: "still readable",
|
|
3997
|
+
detail: `on the ${result2.source} provider until \`penv rotate ${result2.parameter} --complete\``
|
|
3998
|
+
}
|
|
3999
|
+
]);
|
|
4000
|
+
}
|
|
4001
|
+
if (result2.phase === "complete") {
|
|
4002
|
+
return formatRows([
|
|
4003
|
+
{
|
|
4004
|
+
glyph: CHECK,
|
|
4005
|
+
label: "Rotated",
|
|
4006
|
+
subject: result2.parameter,
|
|
4007
|
+
detail: `dual-valid window closed for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
4008
|
+
}
|
|
4009
|
+
]);
|
|
4010
|
+
}
|
|
4011
|
+
return formatRows([
|
|
4012
|
+
{
|
|
4013
|
+
glyph: CHECK,
|
|
4014
|
+
label: "Rotated",
|
|
4015
|
+
subject: result2.parameter,
|
|
4016
|
+
detail: `atomic-cutover flip for environment ${result2.environment} (completed ${result2.lastRotated})`
|
|
4017
|
+
}
|
|
4018
|
+
]);
|
|
4019
|
+
}
|
|
4020
|
+
var rotateCommand = defineCommand16({
|
|
4021
|
+
meta: {
|
|
4022
|
+
name: "rotate",
|
|
4023
|
+
description: "Rotate a parameter's value by the mechanism its meta declares"
|
|
4024
|
+
},
|
|
4025
|
+
args: {
|
|
4026
|
+
key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
|
|
4027
|
+
value: {
|
|
4028
|
+
type: "positional",
|
|
4029
|
+
required: false,
|
|
4030
|
+
description: "The new value; read from stdin if omitted. Not needed with --complete"
|
|
4031
|
+
},
|
|
4032
|
+
env: { type: "string", description: "The environment to rotate in" },
|
|
4033
|
+
begin: { type: "boolean", description: "Open a dual-valid grace window with the new value" },
|
|
4034
|
+
complete: { type: "boolean", description: "Close a dual-valid grace window" }
|
|
4035
|
+
},
|
|
4036
|
+
run({ args }) {
|
|
4037
|
+
return guard(async () => {
|
|
4038
|
+
const value = args.complete === true ? void 0 : args.value ?? await readStdin();
|
|
4039
|
+
write(
|
|
4040
|
+
renderRotate(
|
|
4041
|
+
await runRotate({
|
|
4042
|
+
cwd: process.cwd(),
|
|
4043
|
+
key: args.key,
|
|
4044
|
+
...value === void 0 ? {} : { value },
|
|
4045
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4046
|
+
...args.begin === void 0 ? {} : { begin: args.begin },
|
|
4047
|
+
...args.complete === void 0 ? {} : { complete: args.complete }
|
|
4048
|
+
})
|
|
4049
|
+
)
|
|
4050
|
+
);
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
4054
|
+
|
|
3275
4055
|
// src/commands/watch.ts
|
|
3276
4056
|
import { existsSync as existsSync4, watch } from "fs";
|
|
3277
|
-
import { basename as basename2, dirname as
|
|
4057
|
+
import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
|
|
3278
4058
|
import { schemaFileOf as schemaFileOf4, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
|
|
3279
|
-
import { defineCommand as
|
|
4059
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
3280
4060
|
var DEBOUNCE_MS = 100;
|
|
3281
4061
|
function runWatch(options) {
|
|
3282
4062
|
const project = openProject(options.cwd);
|
|
@@ -3297,12 +4077,12 @@ function runWatch(options) {
|
|
|
3297
4077
|
}
|
|
3298
4078
|
running = true;
|
|
3299
4079
|
try {
|
|
3300
|
-
const
|
|
4080
|
+
const result2 = await runValidate({
|
|
3301
4081
|
cwd: options.cwd,
|
|
3302
4082
|
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
3303
4083
|
});
|
|
3304
4084
|
if (!closed) {
|
|
3305
|
-
options.onResult?.(
|
|
4085
|
+
options.onResult?.(result2);
|
|
3306
4086
|
}
|
|
3307
4087
|
} catch (error) {
|
|
3308
4088
|
if (!closed) {
|
|
@@ -3333,7 +4113,7 @@ function runWatch(options) {
|
|
|
3333
4113
|
watcher.close();
|
|
3334
4114
|
}
|
|
3335
4115
|
function armRecovery(target, recursive, only) {
|
|
3336
|
-
const parent =
|
|
4116
|
+
const parent = dirname4(target);
|
|
3337
4117
|
const name = basename2(target);
|
|
3338
4118
|
let recovery;
|
|
3339
4119
|
try {
|
|
@@ -3407,10 +4187,10 @@ function runWatch(options) {
|
|
|
3407
4187
|
watchers.add(watcher);
|
|
3408
4188
|
}
|
|
3409
4189
|
addWatcher(project.penvDir, true);
|
|
3410
|
-
addWatcher(
|
|
4190
|
+
addWatcher(dirname4(project.configFile), false, configFile);
|
|
3411
4191
|
if (schemaInsideTree2(project.config) === void 0) {
|
|
3412
|
-
const schemaFile =
|
|
3413
|
-
addWatcher(
|
|
4192
|
+
const schemaFile = resolve6(project.root, schemaFileOf4(project.config));
|
|
4193
|
+
addWatcher(dirname4(schemaFile), false, basename2(schemaFile));
|
|
3414
4194
|
}
|
|
3415
4195
|
void validate();
|
|
3416
4196
|
return {
|
|
@@ -3426,8 +4206,8 @@ function runWatch(options) {
|
|
|
3426
4206
|
}
|
|
3427
4207
|
};
|
|
3428
4208
|
}
|
|
3429
|
-
function renderWatch(
|
|
3430
|
-
return ["", ...renderValidate(
|
|
4209
|
+
function renderWatch(result2) {
|
|
4210
|
+
return ["", ...renderValidate(result2), ...renderDrift(result2.drift, result2.environment)];
|
|
3431
4211
|
}
|
|
3432
4212
|
function renderDrift(drift, environment) {
|
|
3433
4213
|
const rows = [
|
|
@@ -3453,7 +4233,7 @@ function renderDrift(drift, environment) {
|
|
|
3453
4233
|
}
|
|
3454
4234
|
return lines;
|
|
3455
4235
|
}
|
|
3456
|
-
var watchCommand =
|
|
4236
|
+
var watchCommand = defineCommand17({
|
|
3457
4237
|
meta: {
|
|
3458
4238
|
name: "watch",
|
|
3459
4239
|
description: "Re-validate whenever .penv/ or penv.config.ts changes"
|
|
@@ -3466,8 +4246,8 @@ var watchCommand = defineCommand14({
|
|
|
3466
4246
|
const handle = runWatch({
|
|
3467
4247
|
cwd: process.cwd(),
|
|
3468
4248
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
3469
|
-
onResult: (
|
|
3470
|
-
write(renderWatch(
|
|
4249
|
+
onResult: (result2) => {
|
|
4250
|
+
write(renderWatch(result2));
|
|
3471
4251
|
},
|
|
3472
4252
|
// A failing cycle is reported and watching continues. `reportError`
|
|
3473
4253
|
// marks the process failed, which `validate` wants and a loop does not:
|
|
@@ -3481,10 +4261,10 @@ var watchCommand = defineCommand14({
|
|
|
3481
4261
|
}
|
|
3482
4262
|
});
|
|
3483
4263
|
write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
|
|
3484
|
-
await new Promise((
|
|
4264
|
+
await new Promise((resolve7) => {
|
|
3485
4265
|
process.once("SIGINT", () => {
|
|
3486
4266
|
handle.close();
|
|
3487
|
-
|
|
4267
|
+
resolve7();
|
|
3488
4268
|
});
|
|
3489
4269
|
});
|
|
3490
4270
|
});
|
|
@@ -3492,7 +4272,7 @@ var watchCommand = defineCommand14({
|
|
|
3492
4272
|
});
|
|
3493
4273
|
|
|
3494
4274
|
// src/index.ts
|
|
3495
|
-
var main =
|
|
4275
|
+
var main = defineCommand18({
|
|
3496
4276
|
meta: {
|
|
3497
4277
|
name: "penv",
|
|
3498
4278
|
description: "Configuration that shares a data model with your production secret manager"
|
|
@@ -3503,8 +4283,11 @@ var main = defineCommand15({
|
|
|
3503
4283
|
generate: generateCommand,
|
|
3504
4284
|
get: getCommand,
|
|
3505
4285
|
set: setCommand,
|
|
4286
|
+
fill: fillCommand,
|
|
3506
4287
|
mv: mvCommand,
|
|
4288
|
+
pull: pullCommand,
|
|
3507
4289
|
push: pushCommand,
|
|
4290
|
+
rotate: rotateCommand,
|
|
3508
4291
|
remove: removeCommand,
|
|
3509
4292
|
list: listCommand,
|
|
3510
4293
|
encrypt: encryptCommand,
|
|
@@ -3526,21 +4309,27 @@ export {
|
|
|
3526
4309
|
insertEnvAlias,
|
|
3527
4310
|
main,
|
|
3528
4311
|
renderDoctor,
|
|
4312
|
+
renderFill,
|
|
3529
4313
|
renderMove,
|
|
4314
|
+
renderPull,
|
|
3530
4315
|
renderPush,
|
|
4316
|
+
renderRotate,
|
|
3531
4317
|
renderWatch,
|
|
3532
4318
|
runDecrypt,
|
|
3533
4319
|
runDoctor,
|
|
3534
4320
|
runEncrypt,
|
|
3535
4321
|
runExplain,
|
|
4322
|
+
runFill,
|
|
3536
4323
|
runGenerate,
|
|
3537
4324
|
runGet,
|
|
3538
4325
|
runInit,
|
|
3539
4326
|
runList,
|
|
3540
4327
|
runMain,
|
|
3541
4328
|
runMove,
|
|
4329
|
+
runPull,
|
|
3542
4330
|
runPush,
|
|
3543
4331
|
runRemove,
|
|
4332
|
+
runRotate,
|
|
3544
4333
|
runSet,
|
|
3545
4334
|
runValidate,
|
|
3546
4335
|
runWatch
|