@penvhq/cli 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1009 -423
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -29
- package/dist/index.d.ts +90 -29
- package/dist/index.js +862 -267
- package/dist/index.js.map +1 -1
- package/package.json +8 -10
package/dist/index.cjs
CHANGED
|
@@ -52,42 +52,93 @@ __export(index_exports, {
|
|
|
52
52
|
runWatch: () => runWatch
|
|
53
53
|
});
|
|
54
54
|
module.exports = __toCommonJS(index_exports);
|
|
55
|
-
var
|
|
55
|
+
var import_core24 = require("@penvhq/core");
|
|
56
56
|
var import_citty18 = require("citty");
|
|
57
57
|
|
|
58
58
|
// src/commands/doctor.ts
|
|
59
|
-
var
|
|
60
|
-
var import_sink_github2 = require("@penvhq/sink-github");
|
|
59
|
+
var import_core8 = require("@penvhq/core");
|
|
61
60
|
var import_citty3 = require("citty");
|
|
62
61
|
|
|
62
|
+
// src/env-flags.ts
|
|
63
|
+
var import_core = require("@penvhq/core");
|
|
64
|
+
var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
|
|
65
|
+
var COMMAND_FLAGS = [
|
|
66
|
+
...BASE_RESERVED,
|
|
67
|
+
"env",
|
|
68
|
+
"out",
|
|
69
|
+
"allow-decrypt",
|
|
70
|
+
"destination",
|
|
71
|
+
"dest",
|
|
72
|
+
"d",
|
|
73
|
+
"location",
|
|
74
|
+
"l",
|
|
75
|
+
"yes",
|
|
76
|
+
"y"
|
|
77
|
+
];
|
|
78
|
+
function shadowedEnvironments(config) {
|
|
79
|
+
const flags = new Set(COMMAND_FLAGS);
|
|
80
|
+
return config.environments.filter((environment) => flags.has(environment));
|
|
81
|
+
}
|
|
82
|
+
function shorthandCandidates(args, declared) {
|
|
83
|
+
const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
|
|
84
|
+
return Object.keys(args).filter((key) => !reserved.has(key) && args[key] === true);
|
|
85
|
+
}
|
|
86
|
+
function quoteList(values) {
|
|
87
|
+
return values.map((value) => `\`--${value}\``).join(", ");
|
|
88
|
+
}
|
|
89
|
+
function environmentFromShorthand(config, candidates, explicit) {
|
|
90
|
+
if (candidates.length === 0) {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
const hits = candidates.filter((candidate) => config.environments.includes(candidate));
|
|
94
|
+
const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
|
|
95
|
+
if (strangers.length > 0) {
|
|
96
|
+
throw new import_core.PenvError(
|
|
97
|
+
"UNKNOWN_FLAG",
|
|
98
|
+
`${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
|
|
99
|
+
`Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (hits.length > 1) {
|
|
103
|
+
throw new import_core.PenvError(
|
|
104
|
+
"ENVIRONMENT_FLAG_AMBIGUOUS",
|
|
105
|
+
`${quoteList(hits)} name two environments at once, and a command acts on exactly one`,
|
|
106
|
+
"Pass a single environment flag, or the canonical `--env <name>`."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const hit = hits[0];
|
|
110
|
+
if (hit !== void 0 && explicit !== void 0 && explicit !== hit) {
|
|
111
|
+
throw new import_core.PenvError(
|
|
112
|
+
"ENVIRONMENT_FLAG_AMBIGUOUS",
|
|
113
|
+
`\`--env ${explicit}\` and \`--${hit}\` name two environments at once`,
|
|
114
|
+
"Drop one of them \u2014 `--env` is the canonical spelling."
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return hit;
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
// src/project.ts
|
|
64
121
|
var import_node_path2 = require("path");
|
|
65
|
-
var
|
|
122
|
+
var import_core3 = require("@penvhq/core");
|
|
66
123
|
var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
|
|
67
124
|
|
|
68
125
|
// src/registry.ts
|
|
69
126
|
var import_node_module = require("module");
|
|
70
127
|
var import_node_path = require("path");
|
|
71
128
|
var import_node_url = require("url");
|
|
72
|
-
var
|
|
129
|
+
var import_core2 = require("@penvhq/core");
|
|
73
130
|
var import_provider_filesystem = require("@penvhq/provider-filesystem");
|
|
74
|
-
var import_provider_kubernetes = require("@penvhq/provider-kubernetes");
|
|
75
131
|
var import_provider_mock = require("@penvhq/provider-mock");
|
|
76
|
-
var import_provider_ssm = require("@penvhq/provider-ssm");
|
|
77
|
-
var import_provider_vault = require("@penvhq/provider-vault");
|
|
78
132
|
var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
|
|
79
|
-
var LOCAL_TREE_TYPE = "filesystem";
|
|
133
|
+
var LOCAL_TREE_TYPE = "@penvhq/provider-filesystem";
|
|
80
134
|
var REGISTRY = /* @__PURE__ */ new Map([
|
|
81
135
|
[LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })],
|
|
82
|
-
["vault", ({ providerConfig }) => (0, import_provider_vault.createVaultProvider)({ path: providerConfig?.path ?? "penv" })],
|
|
83
|
-
["ssm", ({ providerConfig }) => (0, import_provider_ssm.createSsmProvider)({ path: providerConfig?.path ?? "penv" })],
|
|
84
136
|
[
|
|
85
|
-
"
|
|
86
|
-
({
|
|
87
|
-
]
|
|
88
|
-
["mock", ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })]
|
|
137
|
+
"@penvhq/provider-mock",
|
|
138
|
+
({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })
|
|
139
|
+
]
|
|
89
140
|
]);
|
|
90
|
-
var
|
|
141
|
+
var RECORD_CONTRACT_METHODS = [
|
|
91
142
|
"read",
|
|
92
143
|
"write",
|
|
93
144
|
"list",
|
|
@@ -96,6 +147,7 @@ var CONTRACT_METHODS = [
|
|
|
96
147
|
"writeMeta",
|
|
97
148
|
"removeMeta"
|
|
98
149
|
];
|
|
150
|
+
var PROJECTION_CONTRACT_METHODS = ["verify", "push", "list"];
|
|
99
151
|
var pluginModuleCache = /* @__PURE__ */ new Map();
|
|
100
152
|
function isProviderRegistered(type) {
|
|
101
153
|
return REGISTRY.has(type);
|
|
@@ -115,31 +167,30 @@ async function createSourceProvider(type, context) {
|
|
|
115
167
|
}
|
|
116
168
|
async function loadPluginProvider(type, context) {
|
|
117
169
|
const fromDir = resolutionBase(context);
|
|
118
|
-
const
|
|
119
|
-
const resolved = resolvePlugin(specifier, fromDir);
|
|
170
|
+
const resolved = resolvePlugin(type, fromDir);
|
|
120
171
|
if (resolved === void 0) {
|
|
121
|
-
throw unknownProvider(type, context.environment
|
|
172
|
+
throw unknownProvider(type, context.environment);
|
|
122
173
|
}
|
|
123
174
|
let mod;
|
|
124
175
|
try {
|
|
125
176
|
mod = await importPlugin(resolved);
|
|
126
177
|
} catch {
|
|
127
|
-
throw new
|
|
178
|
+
throw new import_core2.PenvError(
|
|
128
179
|
"PROVIDER_PLUGIN_LOAD",
|
|
129
|
-
`The provider package \`${
|
|
180
|
+
`The provider package \`${type}\` failed to load`,
|
|
130
181
|
"It resolved but threw while importing. Check it builds and its dependencies are installed."
|
|
131
182
|
);
|
|
132
183
|
}
|
|
133
184
|
const factory = mod[PLUGIN_FACTORY_EXPORT];
|
|
134
185
|
if (typeof factory !== "function") {
|
|
135
|
-
throw new
|
|
186
|
+
throw new import_core2.PenvError(
|
|
136
187
|
"PROVIDER_PLUGIN_INVALID",
|
|
137
|
-
`\`${
|
|
188
|
+
`\`${type}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
|
|
138
189
|
`A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
|
|
139
190
|
);
|
|
140
191
|
}
|
|
141
192
|
const provider = await factory(context);
|
|
142
|
-
assertSatisfiesContract(provider,
|
|
193
|
+
assertSatisfiesContract(provider, type);
|
|
143
194
|
return provider;
|
|
144
195
|
}
|
|
145
196
|
function assertProvidersRegistered(config, projectRoot) {
|
|
@@ -147,15 +198,11 @@ function assertProvidersRegistered(config, projectRoot) {
|
|
|
147
198
|
if (isProviderRegistered(provider.type)) {
|
|
148
199
|
continue;
|
|
149
200
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
throw unknownProvider(provider.type, environment, specifier);
|
|
201
|
+
if (resolvePlugin(provider.type, projectRoot) === void 0) {
|
|
202
|
+
throw unknownProvider(provider.type, environment);
|
|
153
203
|
}
|
|
154
204
|
}
|
|
155
205
|
}
|
|
156
|
-
function pluginSpecifier(providerConfig, type) {
|
|
157
|
-
return providerConfig?.module ?? `@penvhq/provider-${type}`;
|
|
158
|
-
}
|
|
159
206
|
function resolutionBase(context) {
|
|
160
207
|
return (0, import_node_path.dirname)(context.root);
|
|
161
208
|
}
|
|
@@ -177,39 +224,33 @@ function importPlugin(resolvedPath) {
|
|
|
177
224
|
return loading;
|
|
178
225
|
}
|
|
179
226
|
function assertSatisfiesContract(provider, specifier) {
|
|
180
|
-
|
|
227
|
+
const projection = (0, import_core2.holdsProjection)(provider);
|
|
228
|
+
const methods = projection ? PROJECTION_CONTRACT_METHODS : RECORD_CONTRACT_METHODS;
|
|
229
|
+
const contract = projection ? "the @penvhq/core ProjectionProvider contract its declared capabilities select" : "the @penvhq/core Provider contract that the filesystem provider defines";
|
|
230
|
+
for (const method of methods) {
|
|
181
231
|
if (typeof provider[method] !== "function") {
|
|
182
|
-
throw new
|
|
232
|
+
throw new import_core2.PenvError(
|
|
183
233
|
"PROVIDER_PLUGIN_INVALID",
|
|
184
234
|
`The provider from \`${specifier}\` is missing \`${method}()\``,
|
|
185
|
-
|
|
235
|
+
`It must satisfy ${contract}.`
|
|
186
236
|
);
|
|
187
237
|
}
|
|
188
238
|
}
|
|
189
239
|
}
|
|
190
|
-
function
|
|
191
|
-
const
|
|
192
|
-
const slash = path.indexOf("/");
|
|
193
|
-
if (slash === -1) return { secretName: path };
|
|
194
|
-
const namespace = path.slice(0, slash);
|
|
195
|
-
const secretName = path.slice(slash + 1) || "penv";
|
|
196
|
-
return namespace === "" ? { secretName } : { namespace, secretName };
|
|
197
|
-
}
|
|
198
|
-
function unknownProvider(type, environment, specifier) {
|
|
199
|
-
const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
240
|
+
function unknownProvider(type, environment) {
|
|
241
|
+
const preinstalled = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
|
|
200
242
|
const where = environment === void 0 ? "" : ` for environment ${environment}`;
|
|
201
|
-
|
|
202
|
-
return new import_core.PenvError(
|
|
243
|
+
return new import_core2.PenvError(
|
|
203
244
|
"UNKNOWN_PROVIDER",
|
|
204
|
-
`The provider
|
|
205
|
-
|
|
245
|
+
`The provider \`${type}\`${where} in penv.config.ts is not installed in this project`,
|
|
246
|
+
`Install it with \`npm i ${type}\` \u2014 a provider's \`type\` is the package penv imports. The CLI ships ${preinstalled} pre-installed.`
|
|
206
247
|
);
|
|
207
248
|
}
|
|
208
249
|
|
|
209
250
|
// src/project.ts
|
|
210
251
|
var PENV_DIR = ".penv";
|
|
211
252
|
function openProject(cwd) {
|
|
212
|
-
const { config, file } = (0,
|
|
253
|
+
const { config, file } = (0, import_core3.loadConfig)(cwd);
|
|
213
254
|
const root = (0, import_node_path2.dirname)(file);
|
|
214
255
|
const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
|
|
215
256
|
assertProvidersRegistered(config, root);
|
|
@@ -223,7 +264,7 @@ function openProject(cwd) {
|
|
|
223
264
|
}
|
|
224
265
|
function localTree(project) {
|
|
225
266
|
if (!(project.provider instanceof import_provider_filesystem2.FilesystemProvider)) {
|
|
226
|
-
throw new
|
|
267
|
+
throw new import_core3.PenvError(
|
|
227
268
|
"PROVIDER_NOT_LOCAL",
|
|
228
269
|
`This command reads the local .penv tree synchronously, which the \`${project.provider.type}\` provider is not`,
|
|
229
270
|
"Run this against a filesystem-backed project, or use a command that speaks the async provider contract."
|
|
@@ -243,8 +284,9 @@ async function sourceProviderFor(project, environment) {
|
|
|
243
284
|
environment
|
|
244
285
|
});
|
|
245
286
|
}
|
|
246
|
-
function targetEnvironment(project, explicit) {
|
|
247
|
-
|
|
287
|
+
function targetEnvironment(project, explicit, shorthand) {
|
|
288
|
+
const fromFlags = shorthand === void 0 ? void 0 : environmentFromShorthand(project.config, shorthand, explicit);
|
|
289
|
+
return (0, import_core3.resolveEnvironment)(project.config, explicit ?? fromFlags);
|
|
248
290
|
}
|
|
249
291
|
var KEY_SEPARATOR = /[./\\]/;
|
|
250
292
|
var NO_ENVIRONMENTS = { environments: [], providers: {} };
|
|
@@ -252,56 +294,56 @@ function refFromKey(key, config) {
|
|
|
252
294
|
const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
|
|
253
295
|
const name = segments[segments.length - 1];
|
|
254
296
|
if (name === void 0) {
|
|
255
|
-
throw new
|
|
297
|
+
throw new import_core3.PenvError(
|
|
256
298
|
"PARAMETER_KEY",
|
|
257
299
|
`\`${key}\` names no parameter`,
|
|
258
300
|
"A key is `<namespace>/<name>` or `<namespace>.<name>`, e.g. `redis/password`."
|
|
259
301
|
);
|
|
260
302
|
}
|
|
261
|
-
if ((0,
|
|
262
|
-
throw new
|
|
303
|
+
if ((0, import_core3.isReservedToken)(name, config ?? NO_ENVIRONMENTS)) {
|
|
304
|
+
throw new import_core3.ReservedTokenError("parameter", name, key);
|
|
263
305
|
}
|
|
264
306
|
return { namespace: segments.slice(0, -1), name };
|
|
265
307
|
}
|
|
266
308
|
function assertWritableKey(key) {
|
|
267
309
|
const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
|
|
268
|
-
if (segments.length === 0 || segments.every((s) => (0,
|
|
310
|
+
if (segments.length === 0 || segments.every((s) => (0, import_core3.isCanonicalSegment)(s))) {
|
|
269
311
|
return;
|
|
270
312
|
}
|
|
271
|
-
const ref = (0,
|
|
313
|
+
const ref = (0, import_core3.refFromAccessPath)(segments);
|
|
272
314
|
if (ref !== void 0) {
|
|
273
315
|
const suggestion = [...ref.namespace, ref.name].join("/");
|
|
274
|
-
throw new
|
|
316
|
+
throw new import_core3.PenvError(
|
|
275
317
|
"PARAMETER_KEY_CASING",
|
|
276
318
|
`\`${key}\` is not a canonical parameter name`,
|
|
277
319
|
`Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
|
|
278
320
|
);
|
|
279
321
|
}
|
|
280
|
-
throw new
|
|
322
|
+
throw new import_core3.PenvError(
|
|
281
323
|
"PARAMETER_KEY_UNREACHABLE",
|
|
282
324
|
`No value file can be named that reaches \`${key}\``,
|
|
283
325
|
"Parameter files are lower-case and hyphenated, and this key maps to no such file \u2014 a run of capitals like `apiURL` cannot be reached (use `api-url`, which the schema reads as `apiUrl`). Run `penv validate` or `penv fill` to see the names penv expects."
|
|
284
326
|
);
|
|
285
327
|
}
|
|
286
328
|
function keySourceFor(project, environment) {
|
|
287
|
-
return (0,
|
|
329
|
+
return (0, import_core3.resolveKeySource)(project.config, environment);
|
|
288
330
|
}
|
|
289
331
|
function resolveSync(provider, ref, environment, keys, skipPersonal) {
|
|
290
|
-
for (const file of (0,
|
|
332
|
+
for (const file of (0, import_core3.candidatesFor)(ref, environment, skipPersonal)) {
|
|
291
333
|
const read = provider.readSync(file);
|
|
292
334
|
if (read === void 0) {
|
|
293
335
|
continue;
|
|
294
336
|
}
|
|
295
|
-
const opened = (0,
|
|
337
|
+
const opened = (0, import_core3.openValue)(file, read, keys);
|
|
296
338
|
return {
|
|
297
339
|
ref,
|
|
298
|
-
parameter: (0,
|
|
340
|
+
parameter: (0, import_core3.parameterId)(ref),
|
|
299
341
|
value: opened.kind === "plaintext" ? opened.value : void 0,
|
|
300
342
|
...opened.kind === "failed" ? { undecryptable: opened.failure } : {},
|
|
301
|
-
winner: { file, location: (0,
|
|
343
|
+
winner: { file, location: (0, import_core3.formatValueFile)(file), present: true }
|
|
302
344
|
};
|
|
303
345
|
}
|
|
304
|
-
return { ref, parameter: (0,
|
|
346
|
+
return { ref, parameter: (0, import_core3.parameterId)(ref), value: void 0, winner: void 0 };
|
|
305
347
|
}
|
|
306
348
|
function resolveAllSync(provider, environment, keys, skipPersonal) {
|
|
307
349
|
return refsFrom(provider.listSync()).map(
|
|
@@ -312,20 +354,20 @@ function refsFrom(files) {
|
|
|
312
354
|
const refs = /* @__PURE__ */ new Map();
|
|
313
355
|
for (const file of files) {
|
|
314
356
|
const ref = { namespace: file.namespace, name: file.name };
|
|
315
|
-
const id = (0,
|
|
357
|
+
const id = (0, import_core3.parameterId)(ref);
|
|
316
358
|
if (!refs.has(id)) {
|
|
317
359
|
refs.set(id, ref);
|
|
318
360
|
}
|
|
319
361
|
}
|
|
320
362
|
return [...refs.values()].sort((a, b) => {
|
|
321
|
-
const left = (0,
|
|
322
|
-
const right = (0,
|
|
363
|
+
const left = (0, import_core3.parameterId)(a);
|
|
364
|
+
const right = (0, import_core3.parameterId)(b);
|
|
323
365
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
324
366
|
});
|
|
325
367
|
}
|
|
326
368
|
|
|
327
369
|
// src/schema.ts
|
|
328
|
-
var
|
|
370
|
+
var import_core4 = require("@penvhq/core");
|
|
329
371
|
function defOf(node) {
|
|
330
372
|
if (typeof node !== "object" || node === null) {
|
|
331
373
|
return void 0;
|
|
@@ -396,29 +438,61 @@ function permitsAbsence(node) {
|
|
|
396
438
|
}
|
|
397
439
|
return void 0;
|
|
398
440
|
}
|
|
399
|
-
function leaves(node, path, inherited,
|
|
441
|
+
function leaves(node, path, inherited, out2) {
|
|
400
442
|
const own = permitsAbsence(node);
|
|
401
443
|
const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
|
|
402
444
|
const shape = shapeOf(unwrap(node));
|
|
403
445
|
if (shape === void 0) {
|
|
404
446
|
if (path.length > 0) {
|
|
405
|
-
|
|
447
|
+
out2.push({ path, absencePermitted, node });
|
|
406
448
|
}
|
|
407
449
|
return;
|
|
408
450
|
}
|
|
409
451
|
for (const key of Object.keys(shape)) {
|
|
410
|
-
leaves(shape[key], [...path, key], absencePermitted,
|
|
452
|
+
leaves(shape[key], [...path, key], absencePermitted, out2);
|
|
411
453
|
}
|
|
412
454
|
}
|
|
413
455
|
function combine(left, right) {
|
|
414
456
|
return left === void 0 || right === void 0 ? void 0 : false;
|
|
415
457
|
}
|
|
416
458
|
function declaredLeaves(schema) {
|
|
417
|
-
const
|
|
418
|
-
leaves(schema, [], false,
|
|
419
|
-
return
|
|
459
|
+
const out2 = [];
|
|
460
|
+
leaves(schema, [], false, out2);
|
|
461
|
+
return out2;
|
|
420
462
|
}
|
|
421
|
-
|
|
463
|
+
function defaultValueOf(node) {
|
|
464
|
+
let current = node;
|
|
465
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
466
|
+
const def = defOf(current);
|
|
467
|
+
if (def === void 0) {
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
if (typeOf(current) === "default" || typeOf(current) === "prefault") {
|
|
471
|
+
let value = def.defaultValue;
|
|
472
|
+
if (typeof value === "function") {
|
|
473
|
+
try {
|
|
474
|
+
value = value();
|
|
475
|
+
} catch {
|
|
476
|
+
return void 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (typeof value === "string") {
|
|
480
|
+
return JSON.stringify(value);
|
|
481
|
+
}
|
|
482
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
483
|
+
return String(value);
|
|
484
|
+
}
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
const inner = def.innerType;
|
|
488
|
+
if (inner === void 0) {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
current = inner;
|
|
492
|
+
}
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
var EMPTY_DRIFT = { declared: [], undeclared: [], optional: [] };
|
|
422
496
|
function hasValue(resolution) {
|
|
423
497
|
return resolution.winner !== void 0;
|
|
424
498
|
}
|
|
@@ -426,25 +500,41 @@ function computeDrift(input) {
|
|
|
426
500
|
const { schema, resolutions, config, environment } = input;
|
|
427
501
|
const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
|
|
428
502
|
const declared = [];
|
|
503
|
+
const optional = [];
|
|
429
504
|
for (const leaf of declaredLeaves(schema)) {
|
|
430
|
-
if (leaf.absencePermitted
|
|
505
|
+
if (leaf.absencePermitted === void 0) {
|
|
431
506
|
continue;
|
|
432
507
|
}
|
|
433
508
|
const path = leaf.path.join(".");
|
|
434
|
-
const ref = (0,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
509
|
+
const ref = (0, import_core4.refFromAccessPath)(leaf.path);
|
|
510
|
+
const renameRemedy = `Rename the \`${path}\` key in .penv/env.ts \u2014 a parameter name is lower-case, hyphenated, and never a reserved token, so no value file reaches this key.`;
|
|
511
|
+
if (ref === void 0 || (0, import_core4.isReservedToken)(ref.name, config)) {
|
|
512
|
+
if (leaf.absencePermitted) {
|
|
513
|
+
optional.push({ subject: path, remedy: renameRemedy });
|
|
514
|
+
} else {
|
|
515
|
+
declared.push({
|
|
516
|
+
subject: path,
|
|
517
|
+
remedy: renameRemedy,
|
|
518
|
+
detail: "declared, no filename reaches it"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (valued.has((0, import_core4.parameterId)(ref))) {
|
|
441
524
|
continue;
|
|
442
525
|
}
|
|
443
|
-
if (
|
|
526
|
+
if (leaf.absencePermitted) {
|
|
527
|
+
const defaultValue = defaultValueOf(leaf.node);
|
|
528
|
+
optional.push({
|
|
529
|
+
subject: (0, import_core4.parameterId)(ref),
|
|
530
|
+
ref,
|
|
531
|
+
...defaultValue === void 0 ? {} : { defaultValue },
|
|
532
|
+
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
|
|
533
|
+
});
|
|
444
534
|
continue;
|
|
445
535
|
}
|
|
446
536
|
declared.push({
|
|
447
|
-
subject: (0,
|
|
537
|
+
subject: (0, import_core4.parameterId)(ref),
|
|
448
538
|
ref,
|
|
449
539
|
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`,
|
|
450
540
|
detail: `declared in .penv/env.ts, no value for ${environment}`
|
|
@@ -452,36 +542,99 @@ function computeDrift(input) {
|
|
|
452
542
|
}
|
|
453
543
|
const undeclared = [];
|
|
454
544
|
for (const resolution of resolutions) {
|
|
455
|
-
if (lookup(schema, (0,
|
|
545
|
+
if (lookup(schema, (0, import_core4.accessPath)(resolution.ref)).kind !== "absent") {
|
|
456
546
|
continue;
|
|
457
547
|
}
|
|
458
548
|
undeclared.push({
|
|
459
549
|
ref: resolution.ref,
|
|
460
|
-
variable: (0,
|
|
550
|
+
variable: (0, import_core4.variableName)(resolution.ref, config)
|
|
461
551
|
});
|
|
462
552
|
}
|
|
463
|
-
return { declared, undeclared };
|
|
553
|
+
return { declared, undeclared, optional };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/style.ts
|
|
557
|
+
function supportsColor(stream) {
|
|
558
|
+
const env = process.env;
|
|
559
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") {
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
if (env.TERM === "dumb") {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
return stream.isTTY === true;
|
|
569
|
+
}
|
|
570
|
+
var ESC = `${String.fromCharCode(27)}[`;
|
|
571
|
+
var identity = (text) => text;
|
|
572
|
+
function sgr(open, close, on) {
|
|
573
|
+
if (!on) {
|
|
574
|
+
return identity;
|
|
575
|
+
}
|
|
576
|
+
const opener = `${ESC}${open}m`;
|
|
577
|
+
const closer = `${ESC}${close}m`;
|
|
578
|
+
return (text) => `${opener}${text}${closer}`;
|
|
579
|
+
}
|
|
580
|
+
function paletteFor(stream) {
|
|
581
|
+
const on = supportsColor(stream);
|
|
582
|
+
return {
|
|
583
|
+
enabled: on,
|
|
584
|
+
bold: sgr(1, 22, on),
|
|
585
|
+
dim: sgr(2, 22, on),
|
|
586
|
+
red: sgr(31, 39, on),
|
|
587
|
+
green: sgr(32, 39, on),
|
|
588
|
+
yellow: sgr(33, 39, on),
|
|
589
|
+
cyan: sgr(36, 39, on),
|
|
590
|
+
magenta: sgr(35, 39, on)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
var out = paletteFor(process.stdout);
|
|
594
|
+
var err = paletteFor(process.stderr);
|
|
595
|
+
var STYLE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
596
|
+
function visibleWidth(text) {
|
|
597
|
+
return text.replace(STYLE_PATTERN, "").length;
|
|
598
|
+
}
|
|
599
|
+
function padVisible(text, width) {
|
|
600
|
+
const missing = width - visibleWidth(text);
|
|
601
|
+
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
464
602
|
}
|
|
465
603
|
|
|
466
604
|
// src/ui.ts
|
|
467
|
-
var
|
|
605
|
+
var import_core5 = require("@penvhq/core");
|
|
468
606
|
var CHECK = "\u2713";
|
|
469
607
|
var WARN = "\u26A0";
|
|
608
|
+
var CROSS = "\u2717";
|
|
470
609
|
var UNKNOWN = "?";
|
|
610
|
+
function paintGlyph(glyph) {
|
|
611
|
+
switch (glyph) {
|
|
612
|
+
case CHECK:
|
|
613
|
+
return out.green(glyph);
|
|
614
|
+
case WARN:
|
|
615
|
+
return out.yellow(glyph);
|
|
616
|
+
case CROSS:
|
|
617
|
+
return out.red(glyph);
|
|
618
|
+
case UNKNOWN:
|
|
619
|
+
return out.dim(glyph);
|
|
620
|
+
default:
|
|
621
|
+
return glyph;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
471
624
|
var NOTE_COLUMN = 29;
|
|
472
625
|
function widest(values) {
|
|
473
|
-
return values.reduce((max, value) => Math.max(max, value
|
|
626
|
+
return values.reduce((max, value) => Math.max(max, visibleWidth(value)), 0);
|
|
474
627
|
}
|
|
475
628
|
function formatRows(rows) {
|
|
476
629
|
const labelWidth = widest(rows.map((row) => row.label)) + 2;
|
|
477
630
|
const detailed = rows.filter((row) => row.detail !== void 0);
|
|
478
631
|
const subjectWidth = widest(detailed.map((row) => row.subject ?? "")) + 1;
|
|
479
632
|
return rows.map((row) => {
|
|
480
|
-
const head = `${row.glyph} ${row.label
|
|
633
|
+
const head = `${paintGlyph(row.glyph)} ${padVisible(row.label, labelWidth)}`;
|
|
481
634
|
if (row.detail === void 0) {
|
|
482
635
|
return `${head}${row.subject ?? ""}`.trimEnd();
|
|
483
636
|
}
|
|
484
|
-
return `${head}${(row.subject ?? ""
|
|
637
|
+
return `${head}${padVisible(row.subject ?? "", subjectWidth)}${out.dim(row.detail)}`.trimEnd();
|
|
485
638
|
});
|
|
486
639
|
}
|
|
487
640
|
function columns(rows, gap = 2) {
|
|
@@ -491,18 +644,31 @@ function columns(rows, gap = 2) {
|
|
|
491
644
|
widths.push(widest(rows.map((row) => row[column] ?? "")) + gap);
|
|
492
645
|
}
|
|
493
646
|
return rows.map(
|
|
494
|
-
(row) => row.map(
|
|
647
|
+
(row) => row.map(
|
|
648
|
+
(cell, index) => index === row.length - 1 ? cell : padVisible(cell, widths[index] ?? 0)
|
|
649
|
+
).join("").trimEnd()
|
|
495
650
|
);
|
|
496
651
|
}
|
|
497
652
|
function formatSteps(steps) {
|
|
498
653
|
return steps.map((step) => {
|
|
499
654
|
if (step.note === void 0) {
|
|
500
|
-
return `${step.glyph} ${step.text}`;
|
|
655
|
+
return `${paintGlyph(step.glyph)} ${step.text}`;
|
|
501
656
|
}
|
|
502
|
-
const text = step.text
|
|
503
|
-
return `${step.glyph} ${text}${step.note}`;
|
|
657
|
+
const text = visibleWidth(step.text) >= NOTE_COLUMN ? `${step.text} ` : padVisible(step.text, NOTE_COLUMN);
|
|
658
|
+
return `${paintGlyph(step.glyph)} ${text}${out.dim(step.note)}`;
|
|
504
659
|
});
|
|
505
660
|
}
|
|
661
|
+
function heading(command, context) {
|
|
662
|
+
return context === void 0 ? out.bold(command) : `${out.bold(command)} ${out.dim(`\xB7 ${context}`)}`;
|
|
663
|
+
}
|
|
664
|
+
function tip(text) {
|
|
665
|
+
return ` ${out.cyan("\u2192")} ${text}`;
|
|
666
|
+
}
|
|
667
|
+
function prompt(subject, context) {
|
|
668
|
+
const head = `${out.cyan("?")} ${out.bold(subject)}`;
|
|
669
|
+
const tail = out.dim("\u203A ");
|
|
670
|
+
return context === void 0 ? `${head} ${tail}` : `${head} ${out.dim(`\xB7 ${context}`)} ${tail}`;
|
|
671
|
+
}
|
|
506
672
|
function write(lines) {
|
|
507
673
|
for (const line of lines) {
|
|
508
674
|
process.stdout.write(`${line}
|
|
@@ -510,14 +676,27 @@ function write(lines) {
|
|
|
510
676
|
}
|
|
511
677
|
}
|
|
512
678
|
function reportError(error) {
|
|
513
|
-
if (error instanceof
|
|
514
|
-
|
|
679
|
+
if (error instanceof import_core5.PenvError) {
|
|
680
|
+
const suffix = error.remedy === void 0 ? void 0 : `
|
|
681
|
+
${error.remedy}`;
|
|
682
|
+
const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
|
|
683
|
+
process.stderr.write(`${err.red(CROSS)} ${message}
|
|
684
|
+
`);
|
|
685
|
+
if (error.remedy !== void 0) {
|
|
686
|
+
process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
|
|
515
687
|
`);
|
|
688
|
+
}
|
|
516
689
|
} else if (error instanceof Error) {
|
|
517
|
-
process.stderr.write(`${
|
|
690
|
+
process.stderr.write(`${err.red(CROSS)} ${error.message}
|
|
691
|
+
`);
|
|
692
|
+
const stack = error.stack;
|
|
693
|
+
const frames = stack?.startsWith(`${error.name}: ${error.message}`) ? stack.slice(`${error.name}: ${error.message}`.length).replace(/^\n/, "") : stack;
|
|
694
|
+
if (frames !== void 0 && frames !== "") {
|
|
695
|
+
process.stderr.write(`${err.dim(frames)}
|
|
518
696
|
`);
|
|
697
|
+
}
|
|
519
698
|
} else {
|
|
520
|
-
process.stderr.write(`${String(error)}
|
|
699
|
+
process.stderr.write(`${err.red(CROSS)} ${String(error)}
|
|
521
700
|
`);
|
|
522
701
|
}
|
|
523
702
|
process.exitCode = 1;
|
|
@@ -531,31 +710,86 @@ async function guard(run) {
|
|
|
531
710
|
}
|
|
532
711
|
|
|
533
712
|
// src/commands/push.ts
|
|
534
|
-
var
|
|
535
|
-
var import_sink_github = require("@penvhq/sink-github");
|
|
713
|
+
var import_core6 = require("@penvhq/core");
|
|
536
714
|
var import_citty = require("citty");
|
|
715
|
+
|
|
716
|
+
// src/input.ts
|
|
717
|
+
var import_node_readline = require("readline");
|
|
718
|
+
function lineReader(input = process.stdin, output = process.stdout) {
|
|
719
|
+
const rl = (0, import_node_readline.createInterface)({ input, output });
|
|
720
|
+
const buffered = [];
|
|
721
|
+
const waiting = [];
|
|
722
|
+
let closed = false;
|
|
723
|
+
rl.on("line", (line) => {
|
|
724
|
+
const next = waiting.shift();
|
|
725
|
+
if (next === void 0) {
|
|
726
|
+
buffered.push(line);
|
|
727
|
+
} else {
|
|
728
|
+
next(line);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
rl.on("close", () => {
|
|
732
|
+
closed = true;
|
|
733
|
+
for (const next of waiting.splice(0)) {
|
|
734
|
+
next(void 0);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
return {
|
|
738
|
+
ask(question) {
|
|
739
|
+
const early = buffered.shift();
|
|
740
|
+
if (early !== void 0) {
|
|
741
|
+
return Promise.resolve(early);
|
|
742
|
+
}
|
|
743
|
+
if (closed) {
|
|
744
|
+
return Promise.resolve(void 0);
|
|
745
|
+
}
|
|
746
|
+
rl.setPrompt(question);
|
|
747
|
+
rl.prompt();
|
|
748
|
+
return new Promise((resolve7) => {
|
|
749
|
+
waiting.push(resolve7);
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
close() {
|
|
753
|
+
rl.close();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/commands/push.ts
|
|
537
759
|
var LAST_PUSHED_KEY = "lastPushedAt";
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
760
|
+
async function destinationFor(project, environment, options) {
|
|
761
|
+
if (options.destination !== void 0) {
|
|
762
|
+
const providerConfig = {
|
|
763
|
+
type: options.destination,
|
|
764
|
+
...options.location === void 0 ? {} : { location: options.location }
|
|
765
|
+
};
|
|
766
|
+
const provider2 = options.provider ?? await createSourceProvider(options.destination, {
|
|
767
|
+
root: project.penvDir,
|
|
768
|
+
config: project.config,
|
|
769
|
+
providerConfig,
|
|
770
|
+
environment
|
|
771
|
+
});
|
|
772
|
+
return { provider: provider2, location: options.location };
|
|
773
|
+
}
|
|
774
|
+
const declared = project.config.providers[environment];
|
|
775
|
+
const location = declared?.location;
|
|
776
|
+
if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
|
|
777
|
+
throw new import_core6.PenvError(
|
|
778
|
+
"NO_DESTINATION",
|
|
779
|
+
`Environment ${environment}'s provider is the local .penv tree itself, so penv has nowhere to push`,
|
|
780
|
+
`Declare a provider for it in penv.config.ts \u2014 e.g. \`${environment}: { type: "@penvhq/provider-github", location: "owner/repo" }\` \u2014 or push somewhere once with \`penv push --env ${environment} --destination <package> --location <place>\`.`
|
|
545
781
|
);
|
|
546
782
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return { sink: override, repo };
|
|
550
|
-
}
|
|
551
|
-
if (declared.type === "github") {
|
|
552
|
-
return { sink: (0, import_sink_github.createGithubSink)(repo === void 0 ? {} : { repo }), repo };
|
|
783
|
+
if (options.provider !== void 0) {
|
|
784
|
+
return { provider: options.provider, location };
|
|
553
785
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
786
|
+
const provider = await createSourceProvider(declared.type, {
|
|
787
|
+
root: project.penvDir,
|
|
788
|
+
config: project.config,
|
|
789
|
+
providerConfig: declared,
|
|
790
|
+
environment
|
|
791
|
+
});
|
|
792
|
+
return { provider, location };
|
|
559
793
|
}
|
|
560
794
|
function plan(resolutions, config, environment, allowDecrypt) {
|
|
561
795
|
const outbound = [];
|
|
@@ -567,13 +801,13 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
567
801
|
let encrypted = false;
|
|
568
802
|
if (winner.file.encrypted) {
|
|
569
803
|
if (!allowDecrypt) {
|
|
570
|
-
throw new
|
|
804
|
+
throw new import_core6.PenvError(
|
|
571
805
|
"ENCRYPTED_VALUE_REFUSED",
|
|
572
|
-
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for
|
|
573
|
-
"Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the
|
|
806
|
+
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for the destination to re-seal`,
|
|
807
|
+
"Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the projection; the destination seals it under its own key."
|
|
574
808
|
);
|
|
575
809
|
}
|
|
576
|
-
(0,
|
|
810
|
+
(0, import_core6.requireValue)(resolution, environment);
|
|
577
811
|
encrypted = true;
|
|
578
812
|
}
|
|
579
813
|
if (resolution.value === void 0) {
|
|
@@ -582,7 +816,7 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
582
816
|
const scope = winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment };
|
|
583
817
|
outbound.push({
|
|
584
818
|
ref: resolution.ref,
|
|
585
|
-
variable: (0,
|
|
819
|
+
variable: (0, import_core6.variableName)(resolution.ref, config),
|
|
586
820
|
value: resolution.value,
|
|
587
821
|
scope,
|
|
588
822
|
encrypted
|
|
@@ -600,28 +834,65 @@ function recordPush(tree, ref, environment, iso) {
|
|
|
600
834
|
const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
|
|
601
835
|
tree.writeMetaSync(ref, meta);
|
|
602
836
|
}
|
|
603
|
-
async function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
837
|
+
async function terminalConfirm(question) {
|
|
838
|
+
if (process.stdin.isTTY !== true) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
const reader = lineReader();
|
|
842
|
+
try {
|
|
843
|
+
const answer = await reader.ask(`${question} [y/N] `);
|
|
844
|
+
return answer !== void 0 && /^y(es)?$/i.test(answer.trim());
|
|
845
|
+
} finally {
|
|
846
|
+
reader.close();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function ensureTargetApproved(provider, environment, options) {
|
|
850
|
+
if (provider.targetExists === void 0) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
if (await provider.targetExists(environment)) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
if (provider.ensureTarget === void 0) {
|
|
857
|
+
throw new import_core6.PenvError(
|
|
858
|
+
"MISSING_TARGET",
|
|
859
|
+
`The destination has no environment \`${environment}\` to receive this push, and this provider cannot create one`,
|
|
860
|
+
`Create the environment on the destination side, then run \`penv push --env ${environment}\` again.`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const approved = options.yes === true || await (options.confirm ?? terminalConfirm)(
|
|
864
|
+
`The destination has no environment \`${environment}\`. Create it?`
|
|
865
|
+
);
|
|
866
|
+
if (!approved) {
|
|
867
|
+
throw new import_core6.PenvError(
|
|
868
|
+
"MISSING_TARGET",
|
|
869
|
+
`The destination has no environment \`${environment}\` to receive this push`,
|
|
870
|
+
`Re-run with \`--yes\` to create it, answer \`y\` at the prompt, or create it on the destination side yourself.`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
await provider.ensureTarget(environment);
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
async function pushProjection(project, provider, environment, location, options) {
|
|
607
877
|
const tree = localTree(project);
|
|
608
878
|
const keys = keySourceFor(project, environment);
|
|
609
879
|
const resolutions = resolveAllSync(tree, environment, keys, true);
|
|
610
880
|
const refs = refsFrom(resolutions.map((resolution) => resolution.ref));
|
|
611
|
-
const collision = (0,
|
|
881
|
+
const collision = (0, import_core6.checkNameCollisions)(refs, project.config)[0];
|
|
612
882
|
if (collision !== void 0) {
|
|
613
883
|
throw collision;
|
|
614
884
|
}
|
|
615
|
-
const nameError =
|
|
885
|
+
const nameError = provider.checkNames?.(refs, project.config)[0];
|
|
616
886
|
if (nameError !== void 0) {
|
|
617
887
|
throw nameError;
|
|
618
888
|
}
|
|
619
889
|
const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
|
|
620
|
-
await
|
|
890
|
+
await provider.verify();
|
|
891
|
+
const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
|
|
621
892
|
let repositorySecrets = 0;
|
|
622
893
|
let environmentSecrets = 0;
|
|
623
894
|
for (const item of outbound) {
|
|
624
|
-
await
|
|
895
|
+
await provider.push(item.variable, item.value, item.scope);
|
|
625
896
|
if (item.scope.kind === "repository") {
|
|
626
897
|
repositorySecrets += 1;
|
|
627
898
|
} else {
|
|
@@ -631,15 +902,70 @@ async function runPush(options) {
|
|
|
631
902
|
}
|
|
632
903
|
return {
|
|
633
904
|
environment,
|
|
634
|
-
|
|
905
|
+
destination: provider.type,
|
|
906
|
+
mode: "projection",
|
|
907
|
+
location,
|
|
635
908
|
pushed: outbound.length,
|
|
909
|
+
meta: 0,
|
|
636
910
|
repositorySecrets,
|
|
637
911
|
environmentSecrets,
|
|
638
|
-
decrypted: outbound.filter((item) => item.encrypted).length
|
|
912
|
+
decrypted: outbound.filter((item) => item.encrypted).length,
|
|
913
|
+
createdTarget
|
|
639
914
|
};
|
|
640
915
|
}
|
|
916
|
+
function crossesToRecords(file, environment) {
|
|
917
|
+
const scope = file.scope;
|
|
918
|
+
if (scope.kind === "unscoped") {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
return scope.kind === "environment" && scope.environment === environment;
|
|
922
|
+
}
|
|
923
|
+
async function pushRecords(project, provider, environment, location) {
|
|
924
|
+
const tree = localTree(project);
|
|
925
|
+
const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
|
|
926
|
+
let pushed = 0;
|
|
927
|
+
for (const file of files) {
|
|
928
|
+
const value = tree.readSync(file);
|
|
929
|
+
if (value === void 0) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
await provider.write(file, value);
|
|
933
|
+
pushed += 1;
|
|
934
|
+
}
|
|
935
|
+
const refs = refsFrom(files);
|
|
936
|
+
let meta = 0;
|
|
937
|
+
for (const ref of refs) {
|
|
938
|
+
const block = tree.readMetaSync(ref);
|
|
939
|
+
if (block === void 0) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
await provider.writeMeta(ref, block);
|
|
943
|
+
meta += 1;
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
environment,
|
|
947
|
+
destination: provider.type,
|
|
948
|
+
mode: "records",
|
|
949
|
+
location,
|
|
950
|
+
pushed,
|
|
951
|
+
meta,
|
|
952
|
+
repositorySecrets: 0,
|
|
953
|
+
environmentSecrets: 0,
|
|
954
|
+
decrypted: 0,
|
|
955
|
+
createdTarget: false
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
async function runPush(options) {
|
|
959
|
+
const project = openProject(options.cwd);
|
|
960
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
961
|
+
const { provider, location } = await destinationFor(project, environment, options);
|
|
962
|
+
if ((0, import_core6.holdsProjection)(provider)) {
|
|
963
|
+
return pushProjection(project, provider, environment, location, options);
|
|
964
|
+
}
|
|
965
|
+
return pushRecords(project, provider, environment, location);
|
|
966
|
+
}
|
|
641
967
|
function renderPush(result2) {
|
|
642
|
-
if (result2.pushed === 0) {
|
|
968
|
+
if (result2.pushed === 0 && result2.meta === 0) {
|
|
643
969
|
return formatRows([
|
|
644
970
|
{
|
|
645
971
|
glyph: CHECK,
|
|
@@ -648,7 +974,23 @@ function renderPush(result2) {
|
|
|
648
974
|
}
|
|
649
975
|
]);
|
|
650
976
|
}
|
|
651
|
-
const target =
|
|
977
|
+
const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
|
|
978
|
+
if (result2.mode === "records") {
|
|
979
|
+
return formatRows([
|
|
980
|
+
{
|
|
981
|
+
glyph: CHECK,
|
|
982
|
+
label: "Pushed",
|
|
983
|
+
subject: `${result2.pushed} value ${result2.pushed === 1 ? "file" : "files"}, ${result2.meta} meta`,
|
|
984
|
+
detail: `mirrored verbatim to ${target}`
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
glyph: CHECK,
|
|
988
|
+
label: "Sealed values",
|
|
989
|
+
subject: "crossed byte-for-byte, still sealed",
|
|
990
|
+
detail: "the destination holds envelopes; no key was needed to push them"
|
|
991
|
+
}
|
|
992
|
+
]);
|
|
993
|
+
}
|
|
652
994
|
const rows = [
|
|
653
995
|
{
|
|
654
996
|
glyph: CHECK,
|
|
@@ -663,12 +1005,20 @@ function renderPush(result2) {
|
|
|
663
1005
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
664
1006
|
}
|
|
665
1007
|
];
|
|
1008
|
+
if (result2.createdTarget) {
|
|
1009
|
+
rows.push({
|
|
1010
|
+
glyph: CHECK,
|
|
1011
|
+
label: "Created",
|
|
1012
|
+
subject: `the destination environment ${result2.environment}`,
|
|
1013
|
+
detail: "it did not exist yet; created on your approval"
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
666
1016
|
if (result2.decrypted > 0) {
|
|
667
1017
|
rows.push({
|
|
668
1018
|
glyph: WARN,
|
|
669
1019
|
label: "Decrypted",
|
|
670
1020
|
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
671
|
-
detail: "sent as plaintext for
|
|
1021
|
+
detail: "sent as plaintext for the destination to re-seal under its own key"
|
|
672
1022
|
});
|
|
673
1023
|
}
|
|
674
1024
|
return formatRows(rows);
|
|
@@ -676,13 +1026,28 @@ function renderPush(result2) {
|
|
|
676
1026
|
var pushCommand = (0, import_citty.defineCommand)({
|
|
677
1027
|
meta: {
|
|
678
1028
|
name: "push",
|
|
679
|
-
description: "Push an environment's
|
|
1029
|
+
description: "Push an environment's values to its provider"
|
|
680
1030
|
},
|
|
681
1031
|
args: {
|
|
682
1032
|
env: { type: "string", description: "The environment to push" },
|
|
683
1033
|
"allow-decrypt": {
|
|
684
1034
|
type: "boolean",
|
|
685
|
-
description: "Decrypt sealed values locally and push them as plaintext for
|
|
1035
|
+
description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
|
|
1036
|
+
},
|
|
1037
|
+
destination: {
|
|
1038
|
+
type: "string",
|
|
1039
|
+
alias: ["dest", "d"],
|
|
1040
|
+
description: "Push once to this provider package instead of the declared one"
|
|
1041
|
+
},
|
|
1042
|
+
location: {
|
|
1043
|
+
type: "string",
|
|
1044
|
+
alias: "l",
|
|
1045
|
+
description: "The destination-side place, when --destination needs one"
|
|
1046
|
+
},
|
|
1047
|
+
yes: {
|
|
1048
|
+
type: "boolean",
|
|
1049
|
+
alias: "y",
|
|
1050
|
+
description: "Create a missing destination-side environment without prompting"
|
|
686
1051
|
}
|
|
687
1052
|
},
|
|
688
1053
|
run({ args }) {
|
|
@@ -690,7 +1055,22 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
690
1055
|
const result2 = await runPush({
|
|
691
1056
|
cwd: process.cwd(),
|
|
692
1057
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
693
|
-
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
1058
|
+
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
|
|
1059
|
+
...args.destination === void 0 ? {} : { destination: args.destination },
|
|
1060
|
+
...args.location === void 0 ? {} : { location: args.location },
|
|
1061
|
+
...args.yes === void 0 ? {} : { yes: args.yes },
|
|
1062
|
+
envFlags: shorthandCandidates(args, [
|
|
1063
|
+
"env",
|
|
1064
|
+
"allow-decrypt",
|
|
1065
|
+
"allowDecrypt",
|
|
1066
|
+
"destination",
|
|
1067
|
+
"dest",
|
|
1068
|
+
"d",
|
|
1069
|
+
"location",
|
|
1070
|
+
"l",
|
|
1071
|
+
"yes",
|
|
1072
|
+
"y"
|
|
1073
|
+
])
|
|
694
1074
|
});
|
|
695
1075
|
write(renderPush(result2));
|
|
696
1076
|
});
|
|
@@ -700,7 +1080,7 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
700
1080
|
// src/commands/validate.ts
|
|
701
1081
|
var import_node_path3 = require("path");
|
|
702
1082
|
var import_node_url2 = require("url");
|
|
703
|
-
var
|
|
1083
|
+
var import_core7 = require("@penvhq/core");
|
|
704
1084
|
var import_citty2 = require("citty");
|
|
705
1085
|
var SCHEMA_EXPORT = "schema";
|
|
706
1086
|
var LABELS = {
|
|
@@ -718,10 +1098,10 @@ function issueFrom(error, fallbackSubject) {
|
|
|
718
1098
|
message: firstLine(error.message),
|
|
719
1099
|
...error.remedy === void 0 ? {} : { remedy: error.remedy }
|
|
720
1100
|
};
|
|
721
|
-
if (error instanceof
|
|
1101
|
+
if (error instanceof import_core7.ReservedTokenError) {
|
|
722
1102
|
return { kind: "reserved", subject: error.token, ...base };
|
|
723
1103
|
}
|
|
724
|
-
if (error instanceof
|
|
1104
|
+
if (error instanceof import_core7.NameCollisionError) {
|
|
725
1105
|
return { kind: "collision", subject: error.variable, ...base };
|
|
726
1106
|
}
|
|
727
1107
|
return { kind: "config", subject: fallbackSubject, ...base };
|
|
@@ -775,16 +1155,16 @@ function exclusively(work) {
|
|
|
775
1155
|
return result2;
|
|
776
1156
|
}
|
|
777
1157
|
function loadSchema(project, environment) {
|
|
778
|
-
const schemaPath = (0,
|
|
1158
|
+
const schemaPath = (0, import_core7.schemaFileOf)(project.config);
|
|
779
1159
|
const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
|
|
780
1160
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
781
1161
|
}
|
|
782
1162
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
783
|
-
const jiti = (0,
|
|
1163
|
+
const jiti = (0, import_core7.jitiFor)(file);
|
|
784
1164
|
const previous = process.env.PENV_ENV;
|
|
785
|
-
const previousHarvest = process.env[
|
|
1165
|
+
const previousHarvest = process.env[import_core7.SCHEMA_HARVEST_ENV];
|
|
786
1166
|
process.env.PENV_ENV = environment;
|
|
787
|
-
process.env[
|
|
1167
|
+
process.env[import_core7.SCHEMA_HARVEST_ENV] = "1";
|
|
788
1168
|
let loaded;
|
|
789
1169
|
try {
|
|
790
1170
|
loaded = await jiti.import(file);
|
|
@@ -811,9 +1191,9 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
811
1191
|
process.env.PENV_ENV = previous;
|
|
812
1192
|
}
|
|
813
1193
|
if (previousHarvest === void 0) {
|
|
814
|
-
delete process.env[
|
|
1194
|
+
delete process.env[import_core7.SCHEMA_HARVEST_ENV];
|
|
815
1195
|
} else {
|
|
816
|
-
process.env[
|
|
1196
|
+
process.env[import_core7.SCHEMA_HARVEST_ENV] = previousHarvest;
|
|
817
1197
|
}
|
|
818
1198
|
}
|
|
819
1199
|
const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
|
|
@@ -833,14 +1213,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
833
1213
|
}
|
|
834
1214
|
async function runValidate(options) {
|
|
835
1215
|
const project = openProject(options.cwd);
|
|
836
|
-
const environment = targetEnvironment(project, options.environment);
|
|
837
|
-
const schemaPath = (0,
|
|
1216
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
1217
|
+
const schemaPath = (0, import_core7.schemaFileOf)(project.config);
|
|
838
1218
|
const issues = [];
|
|
839
1219
|
let files;
|
|
840
1220
|
try {
|
|
841
1221
|
files = await project.provider.list();
|
|
842
1222
|
} catch (error) {
|
|
843
|
-
if (!(error instanceof
|
|
1223
|
+
if (!(error instanceof import_core7.PenvError)) {
|
|
844
1224
|
throw error;
|
|
845
1225
|
}
|
|
846
1226
|
return {
|
|
@@ -852,17 +1232,17 @@ async function runValidate(options) {
|
|
|
852
1232
|
};
|
|
853
1233
|
}
|
|
854
1234
|
const refs = refsFrom(files);
|
|
855
|
-
for (const error of (0,
|
|
1235
|
+
for (const error of (0, import_core7.validateConfig)(project.config)) {
|
|
856
1236
|
issues.push(issueFrom(error, "penv.config.ts"));
|
|
857
1237
|
}
|
|
858
|
-
for (const collision of (0,
|
|
1238
|
+
for (const collision of (0, import_core7.checkNameCollisions)(refs, project.config)) {
|
|
859
1239
|
issues.push(issueFrom(collision, collision.variable));
|
|
860
1240
|
}
|
|
861
1241
|
const { schema, issues: schemaIssues } = await loadSchema(project, environment);
|
|
862
1242
|
issues.push(...schemaIssues);
|
|
863
1243
|
let drift = EMPTY_DRIFT;
|
|
864
1244
|
if (schema !== void 0) {
|
|
865
|
-
const resolutions = await (0,
|
|
1245
|
+
const resolutions = await (0, import_core7.resolveAll)(
|
|
866
1246
|
environment,
|
|
867
1247
|
project.provider,
|
|
868
1248
|
keySourceFor(project, environment)
|
|
@@ -871,7 +1251,7 @@ async function runValidate(options) {
|
|
|
871
1251
|
const undecryptable = /* @__PURE__ */ new Set();
|
|
872
1252
|
for (const resolution of resolutions) {
|
|
873
1253
|
if (resolution.undecryptable !== void 0) {
|
|
874
|
-
undecryptable.add((0,
|
|
1254
|
+
undecryptable.add((0, import_core7.accessPath)(resolution.ref).join("."));
|
|
875
1255
|
issues.push({
|
|
876
1256
|
kind: "undecryptable",
|
|
877
1257
|
subject: resolution.parameter,
|
|
@@ -881,7 +1261,7 @@ async function runValidate(options) {
|
|
|
881
1261
|
continue;
|
|
882
1262
|
}
|
|
883
1263
|
if (resolution.value !== void 0) {
|
|
884
|
-
place(object, (0,
|
|
1264
|
+
place(object, (0, import_core7.accessPath)(resolution.ref), resolution.value);
|
|
885
1265
|
}
|
|
886
1266
|
}
|
|
887
1267
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
@@ -914,18 +1294,26 @@ function renderValidate(result2) {
|
|
|
914
1294
|
]);
|
|
915
1295
|
}
|
|
916
1296
|
const rows = result2.issues.map((issue) => ({
|
|
917
|
-
glyph:
|
|
1297
|
+
glyph: CROSS,
|
|
918
1298
|
label: LABELS[issue.kind],
|
|
919
1299
|
subject: issue.subject,
|
|
920
1300
|
detail: issue.message
|
|
921
1301
|
}));
|
|
922
1302
|
const lines = formatRows(rows);
|
|
923
|
-
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))]
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1303
|
+
const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))].filter(
|
|
1304
|
+
(remedy) => remedy !== void 0
|
|
1305
|
+
);
|
|
1306
|
+
if (remedies.length > 0) {
|
|
1307
|
+
lines.push("");
|
|
1308
|
+
for (const remedy of remedies) {
|
|
1309
|
+
lines.push(tip(remedy));
|
|
927
1310
|
}
|
|
928
1311
|
}
|
|
1312
|
+
const count = result2.issues.length;
|
|
1313
|
+
lines.push(
|
|
1314
|
+
"",
|
|
1315
|
+
`${out.red(CROSS)} ${count} ${count === 1 ? "issue" : "issues"} for environment ${result2.environment}`
|
|
1316
|
+
);
|
|
929
1317
|
return lines;
|
|
930
1318
|
}
|
|
931
1319
|
var validateCommand = (0, import_citty2.defineCommand)({
|
|
@@ -940,7 +1328,8 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
940
1328
|
return guard(async () => {
|
|
941
1329
|
const result2 = await runValidate({
|
|
942
1330
|
cwd: process.cwd(),
|
|
943
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
1331
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
1332
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
944
1333
|
});
|
|
945
1334
|
write(renderValidate(result2));
|
|
946
1335
|
if (!result2.ok) {
|
|
@@ -963,10 +1352,19 @@ function skipped(check, label) {
|
|
|
963
1352
|
}
|
|
964
1353
|
async function runDoctor(options) {
|
|
965
1354
|
const project = openProject(options.cwd);
|
|
966
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1355
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
967
1356
|
const findings = [];
|
|
968
1357
|
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
969
1358
|
const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
1359
|
+
for (const shadowed of shadowedEnvironments(project.config)) {
|
|
1360
|
+
findings.push({
|
|
1361
|
+
check: "environment-flag-shadow",
|
|
1362
|
+
severity: "warning",
|
|
1363
|
+
label: "Environment shadows a flag",
|
|
1364
|
+
subject: shadowed,
|
|
1365
|
+
detail: `\`--${shadowed}\` is a flag penv defines, so this environment has no shorthand \u2014 \`--env ${shadowed}\` always works`
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
970
1368
|
const { schema, issues } = await loadSchema(project, environment);
|
|
971
1369
|
if (schema !== void 0) {
|
|
972
1370
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -981,7 +1379,7 @@ async function runDoctor(options) {
|
|
|
981
1379
|
});
|
|
982
1380
|
}
|
|
983
1381
|
}
|
|
984
|
-
const resolutions = await (0,
|
|
1382
|
+
const resolutions = await (0, import_core8.resolveAll)(
|
|
985
1383
|
environment,
|
|
986
1384
|
project.provider,
|
|
987
1385
|
keySourceFor(project, environment)
|
|
@@ -1023,7 +1421,7 @@ async function runDoctor(options) {
|
|
|
1023
1421
|
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1024
1422
|
}
|
|
1025
1423
|
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
1026
|
-
findings.push(...await
|
|
1424
|
+
findings.push(...await projectionFindings(project, environment, options.projection));
|
|
1027
1425
|
findings.push({
|
|
1028
1426
|
check: "provider",
|
|
1029
1427
|
severity: "pass",
|
|
@@ -1037,7 +1435,7 @@ async function runDoctor(options) {
|
|
|
1037
1435
|
};
|
|
1038
1436
|
}
|
|
1039
1437
|
function missingFindings(subjects, environment) {
|
|
1040
|
-
const required = subjects.filter(({ meta }) => (0,
|
|
1438
|
+
const required = subjects.filter(({ meta }) => (0, import_core8.isRequired)(meta, environment));
|
|
1041
1439
|
const findings = required.filter(({ resolution }) => resolution.winner === void 0).map(({ resolution }) => ({
|
|
1042
1440
|
check: "missing",
|
|
1043
1441
|
severity: "failure",
|
|
@@ -1088,7 +1486,7 @@ function weakFindings(subjects, schema) {
|
|
|
1088
1486
|
if (value === void 0) {
|
|
1089
1487
|
continue;
|
|
1090
1488
|
}
|
|
1091
|
-
const field = lookup(schema, (0,
|
|
1489
|
+
const field = lookup(schema, (0, import_core8.accessPath)(resolution.ref));
|
|
1092
1490
|
if (field.kind !== "found") {
|
|
1093
1491
|
continue;
|
|
1094
1492
|
}
|
|
@@ -1161,7 +1559,7 @@ function fallbackFindings(subjects, environment) {
|
|
|
1161
1559
|
];
|
|
1162
1560
|
}
|
|
1163
1561
|
function plaintextSecretFindings(subjects, environment) {
|
|
1164
|
-
const secrets = subjects.filter(({ meta }) => (0,
|
|
1562
|
+
const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
|
|
1165
1563
|
const findings = [];
|
|
1166
1564
|
for (const { resolution } of secrets) {
|
|
1167
1565
|
const winner = resolution.winner;
|
|
@@ -1201,11 +1599,11 @@ function publicSecretFindings(subjects, environment, config) {
|
|
|
1201
1599
|
}
|
|
1202
1600
|
];
|
|
1203
1601
|
}
|
|
1204
|
-
const secrets = subjects.filter(({ meta }) => (0,
|
|
1602
|
+
const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
|
|
1205
1603
|
const findings = [];
|
|
1206
1604
|
for (const { resolution } of secrets) {
|
|
1207
|
-
const variable = (0,
|
|
1208
|
-
if (!(0,
|
|
1605
|
+
const variable = (0, import_core8.variableName)(resolution.ref, config);
|
|
1606
|
+
if (!(0, import_core8.isPublicVariable)(variable, config)) {
|
|
1209
1607
|
continue;
|
|
1210
1608
|
}
|
|
1211
1609
|
const prefix = prefixes.find((candidate) => variable.startsWith(candidate));
|
|
@@ -1259,15 +1657,6 @@ function encryptionFindings(subjects, environment) {
|
|
|
1259
1657
|
];
|
|
1260
1658
|
}
|
|
1261
1659
|
var EDIT_SKEW_MS = 12e4;
|
|
1262
|
-
function buildSink(declared, override) {
|
|
1263
|
-
if (override !== void 0) {
|
|
1264
|
-
return override;
|
|
1265
|
-
}
|
|
1266
|
-
if (declared.type === "github") {
|
|
1267
|
-
return (0, import_sink_github2.createGithubSink)(declared.repo === void 0 ? {} : { repo: declared.repo });
|
|
1268
|
-
}
|
|
1269
|
-
return void 0;
|
|
1270
|
-
}
|
|
1271
1660
|
function errorDetail(error) {
|
|
1272
1661
|
if (error instanceof Error) {
|
|
1273
1662
|
return error.message.split("\n")[0] ?? error.message;
|
|
@@ -1277,32 +1666,43 @@ function errorDetail(error) {
|
|
|
1277
1666
|
function scopeLabel(scope) {
|
|
1278
1667
|
return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
|
|
1279
1668
|
}
|
|
1280
|
-
async function
|
|
1281
|
-
|
|
1282
|
-
if (
|
|
1283
|
-
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1669
|
+
async function projectionFindings(project, environment, override) {
|
|
1670
|
+
let projection;
|
|
1671
|
+
if (override !== void 0) {
|
|
1672
|
+
projection = override;
|
|
1673
|
+
} else {
|
|
1674
|
+
const declared = project.config.providers[environment];
|
|
1675
|
+
if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
|
|
1676
|
+
return [];
|
|
1677
|
+
}
|
|
1678
|
+
let source;
|
|
1679
|
+
try {
|
|
1680
|
+
source = await sourceProviderFor(project, environment);
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
return [
|
|
1683
|
+
{
|
|
1684
|
+
check: "projection-unreachable",
|
|
1685
|
+
severity: "unknown",
|
|
1686
|
+
label: "Destination",
|
|
1687
|
+
subject: `could not build the ${declared.type} provider for ${environment}`,
|
|
1688
|
+
detail: errorDetail(error)
|
|
1689
|
+
}
|
|
1690
|
+
];
|
|
1691
|
+
}
|
|
1692
|
+
if (!(0, import_core8.holdsProjection)(source)) {
|
|
1693
|
+
return [];
|
|
1694
|
+
}
|
|
1695
|
+
projection = source;
|
|
1296
1696
|
}
|
|
1297
1697
|
try {
|
|
1298
|
-
await
|
|
1698
|
+
await projection.verify();
|
|
1299
1699
|
} catch (error) {
|
|
1300
1700
|
return [
|
|
1301
1701
|
{
|
|
1302
|
-
check: "
|
|
1702
|
+
check: "projection-unreachable",
|
|
1303
1703
|
severity: "unknown",
|
|
1304
|
-
label: "
|
|
1305
|
-
subject: `could not reach the ${
|
|
1704
|
+
label: "Destination",
|
|
1705
|
+
subject: `could not reach the ${projection.type} destination for ${environment}`,
|
|
1306
1706
|
detail: errorDetail(error)
|
|
1307
1707
|
}
|
|
1308
1708
|
];
|
|
@@ -1310,20 +1710,20 @@ async function sinkFindings(project, environment, override) {
|
|
|
1310
1710
|
let repoSecrets;
|
|
1311
1711
|
let envSecrets;
|
|
1312
1712
|
try {
|
|
1313
|
-
repoSecrets = await
|
|
1314
|
-
envSecrets = await
|
|
1713
|
+
repoSecrets = await projection.list({ kind: "repository" });
|
|
1714
|
+
envSecrets = await projection.list({ kind: "environment", environment });
|
|
1315
1715
|
} catch (error) {
|
|
1316
1716
|
return [
|
|
1317
1717
|
{
|
|
1318
|
-
check: "
|
|
1718
|
+
check: "projection-unreachable",
|
|
1319
1719
|
severity: "unknown",
|
|
1320
|
-
label: "
|
|
1321
|
-
subject: `could not list secrets in the ${
|
|
1720
|
+
label: "Destination",
|
|
1721
|
+
subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
|
|
1322
1722
|
detail: errorDetail(error)
|
|
1323
1723
|
}
|
|
1324
1724
|
];
|
|
1325
1725
|
}
|
|
1326
|
-
const resolutions = await (0,
|
|
1726
|
+
const resolutions = await (0, import_core8.resolveAll)(
|
|
1327
1727
|
environment,
|
|
1328
1728
|
project.provider,
|
|
1329
1729
|
keySourceFor(project, environment),
|
|
@@ -1337,7 +1737,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1337
1737
|
}
|
|
1338
1738
|
expected.push({
|
|
1339
1739
|
ref: resolution.ref,
|
|
1340
|
-
variable: (0,
|
|
1740
|
+
variable: (0, import_core8.variableName)(resolution.ref, project.config),
|
|
1341
1741
|
scope: winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment }
|
|
1342
1742
|
});
|
|
1343
1743
|
}
|
|
@@ -1356,7 +1756,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1356
1756
|
}
|
|
1357
1757
|
if (!destOf(item.scope).has(key)) {
|
|
1358
1758
|
nameDrift.push({
|
|
1359
|
-
check: "
|
|
1759
|
+
check: "projection-name-drift",
|
|
1360
1760
|
severity: "warning",
|
|
1361
1761
|
label: "Declared, not pushed",
|
|
1362
1762
|
subject: item.variable,
|
|
@@ -1368,7 +1768,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1368
1768
|
for (const secret of repoSecrets) {
|
|
1369
1769
|
if (!allVariables.has(upper(secret.name))) {
|
|
1370
1770
|
nameDrift.push({
|
|
1371
|
-
check: "
|
|
1771
|
+
check: "projection-name-drift",
|
|
1372
1772
|
severity: "warning",
|
|
1373
1773
|
label: "In destination, not declared",
|
|
1374
1774
|
subject: secret.name,
|
|
@@ -1379,7 +1779,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1379
1779
|
for (const secret of envSecrets) {
|
|
1380
1780
|
if (!expectedEnv.has(upper(secret.name))) {
|
|
1381
1781
|
nameDrift.push({
|
|
1382
|
-
check: "
|
|
1782
|
+
check: "projection-name-drift",
|
|
1383
1783
|
severity: "warning",
|
|
1384
1784
|
label: "In destination, not declared",
|
|
1385
1785
|
subject: secret.name,
|
|
@@ -1393,7 +1793,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1393
1793
|
if (secret === void 0) {
|
|
1394
1794
|
continue;
|
|
1395
1795
|
}
|
|
1396
|
-
const pushed = (0,
|
|
1796
|
+
const pushed = (0, import_core8.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
|
|
1397
1797
|
if (typeof pushed !== "string") {
|
|
1398
1798
|
continue;
|
|
1399
1799
|
}
|
|
@@ -1403,7 +1803,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1403
1803
|
continue;
|
|
1404
1804
|
}
|
|
1405
1805
|
manualEdits.push({
|
|
1406
|
-
check: "
|
|
1806
|
+
check: "projection-manual-edit",
|
|
1407
1807
|
severity: "warning",
|
|
1408
1808
|
label: "Edited outside penv",
|
|
1409
1809
|
subject: item.variable,
|
|
@@ -1414,9 +1814,9 @@ async function sinkFindings(project, environment, override) {
|
|
|
1414
1814
|
findings.push(
|
|
1415
1815
|
...nameDrift.length > 0 ? nameDrift : [
|
|
1416
1816
|
{
|
|
1417
|
-
check: "
|
|
1817
|
+
check: "projection-name-drift",
|
|
1418
1818
|
severity: "pass",
|
|
1419
|
-
label: "
|
|
1819
|
+
label: "Destination names",
|
|
1420
1820
|
subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
|
|
1421
1821
|
}
|
|
1422
1822
|
]
|
|
@@ -1424,17 +1824,17 @@ async function sinkFindings(project, environment, override) {
|
|
|
1424
1824
|
findings.push(
|
|
1425
1825
|
...manualEdits.length > 0 ? manualEdits : [
|
|
1426
1826
|
{
|
|
1427
|
-
check: "
|
|
1827
|
+
check: "projection-manual-edit",
|
|
1428
1828
|
severity: "pass",
|
|
1429
|
-
label: "
|
|
1829
|
+
label: "Destination hand-edits",
|
|
1430
1830
|
subject: `no secret has changed outside penv since its last push for ${environment}`
|
|
1431
1831
|
}
|
|
1432
1832
|
]
|
|
1433
1833
|
);
|
|
1434
1834
|
findings.push({
|
|
1435
|
-
check: "
|
|
1835
|
+
check: "projection-value-drift",
|
|
1436
1836
|
severity: "unknown",
|
|
1437
|
-
label: "
|
|
1837
|
+
label: "Destination values",
|
|
1438
1838
|
subject: "cannot be read back from a write-only destination",
|
|
1439
1839
|
detail: "value drift between the tree and the destination is unknowable by design"
|
|
1440
1840
|
});
|
|
@@ -1463,6 +1863,9 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1463
1863
|
return { kind: "read", subjects: local };
|
|
1464
1864
|
}
|
|
1465
1865
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
1866
|
+
if (!(0, import_core8.holdsRecords)(source)) {
|
|
1867
|
+
return { kind: "read", subjects: local };
|
|
1868
|
+
}
|
|
1466
1869
|
try {
|
|
1467
1870
|
const subjects = await Promise.all(
|
|
1468
1871
|
local.map(async ({ resolution }) => ({
|
|
@@ -1487,11 +1890,11 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1487
1890
|
function overdueFindings(subjects, environment, now) {
|
|
1488
1891
|
const findings = [];
|
|
1489
1892
|
for (const { resolution, meta } of subjects) {
|
|
1490
|
-
const { policy, lastRotated } = (0,
|
|
1893
|
+
const { policy, lastRotated } = (0, import_core8.rotationOf)(meta, environment);
|
|
1491
1894
|
if (policy === void 0 || lastRotated === null) {
|
|
1492
1895
|
continue;
|
|
1493
1896
|
}
|
|
1494
|
-
const interval = (0,
|
|
1897
|
+
const interval = (0, import_core8.tryParseDuration)(policy);
|
|
1495
1898
|
if (interval === void 0) {
|
|
1496
1899
|
findings.push({
|
|
1497
1900
|
check: "rotation-overdue",
|
|
@@ -1534,10 +1937,10 @@ function overdueFindings(subjects, environment, now) {
|
|
|
1534
1937
|
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1535
1938
|
const findings = [];
|
|
1536
1939
|
for (const { resolution, meta } of subjects) {
|
|
1537
|
-
if (!(0,
|
|
1940
|
+
if (!(0, import_core8.isStuck)(meta, environment, now, stuckThresholdMs)) {
|
|
1538
1941
|
continue;
|
|
1539
1942
|
}
|
|
1540
|
-
const { rotatingSince } = (0,
|
|
1943
|
+
const { rotatingSince } = (0, import_core8.rotationOf)(meta, environment);
|
|
1541
1944
|
if (rotatingSince === null) {
|
|
1542
1945
|
continue;
|
|
1543
1946
|
}
|
|
@@ -1577,7 +1980,7 @@ function scopeKey(scope) {
|
|
|
1577
1980
|
case "environment-local":
|
|
1578
1981
|
return `environment-local:${scope.environment}`;
|
|
1579
1982
|
default:
|
|
1580
|
-
return (0,
|
|
1983
|
+
return (0, import_core8.assertNever)(scope, "scope");
|
|
1581
1984
|
}
|
|
1582
1985
|
}
|
|
1583
1986
|
function relevantToEnvironment(file, environment) {
|
|
@@ -1611,6 +2014,9 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1611
2014
|
];
|
|
1612
2015
|
}
|
|
1613
2016
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
2017
|
+
if (!(0, import_core8.holdsRecords)(source)) {
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
1614
2020
|
let local;
|
|
1615
2021
|
let remote;
|
|
1616
2022
|
try {
|
|
@@ -1634,13 +2040,13 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1634
2040
|
const here = local.get(address);
|
|
1635
2041
|
const there = remote.get(address);
|
|
1636
2042
|
if (here !== void 0 && there !== void 0) {
|
|
1637
|
-
const opened = (0,
|
|
2043
|
+
const opened = (0, import_core8.openValue)(here.file, here.stored, keys);
|
|
1638
2044
|
if (opened.kind !== "plaintext") {
|
|
1639
2045
|
findings.push({
|
|
1640
2046
|
check: "provider-value-drift",
|
|
1641
2047
|
severity: "unknown",
|
|
1642
2048
|
label: "Provider value unreadable",
|
|
1643
|
-
subject: (0,
|
|
2049
|
+
subject: (0, import_core8.formatValueFile)(here.file),
|
|
1644
2050
|
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1645
2051
|
});
|
|
1646
2052
|
continue;
|
|
@@ -1650,7 +2056,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1650
2056
|
check: "provider-value-drift",
|
|
1651
2057
|
severity: "failure",
|
|
1652
2058
|
label: "Provider value drift",
|
|
1653
|
-
subject: (0,
|
|
2059
|
+
subject: (0, import_core8.formatValueFile)(here.file),
|
|
1654
2060
|
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1655
2061
|
});
|
|
1656
2062
|
}
|
|
@@ -1664,7 +2070,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1664
2070
|
check: "provider-value-drift",
|
|
1665
2071
|
severity: "warning",
|
|
1666
2072
|
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1667
|
-
subject: (0,
|
|
2073
|
+
subject: (0, import_core8.formatValueFile)(present.file),
|
|
1668
2074
|
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`
|
|
1669
2075
|
});
|
|
1670
2076
|
}
|
|
@@ -1680,28 +2086,57 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1680
2086
|
}
|
|
1681
2087
|
];
|
|
1682
2088
|
}
|
|
2089
|
+
var GLYPHS = {
|
|
2090
|
+
pass: CHECK,
|
|
2091
|
+
warning: WARN,
|
|
2092
|
+
failure: CROSS,
|
|
2093
|
+
unknown: UNKNOWN
|
|
2094
|
+
};
|
|
2095
|
+
function summarize(report) {
|
|
2096
|
+
const counts = { pass: 0, warning: 0, failure: 0, unknown: 0 };
|
|
2097
|
+
for (const finding of report.findings) {
|
|
2098
|
+
counts[finding.severity] += 1;
|
|
2099
|
+
}
|
|
2100
|
+
const parts = [out.green(`${counts.pass} passed`)];
|
|
2101
|
+
if (counts.warning > 0) {
|
|
2102
|
+
parts.push(out.yellow(`${counts.warning} ${counts.warning === 1 ? "warning" : "warnings"}`));
|
|
2103
|
+
}
|
|
2104
|
+
if (counts.failure > 0) {
|
|
2105
|
+
parts.push(out.red(`${counts.failure} ${counts.failure === 1 ? "failure" : "failures"}`));
|
|
2106
|
+
}
|
|
2107
|
+
if (counts.unknown > 0) {
|
|
2108
|
+
parts.push(out.dim(`${counts.unknown} could not be checked`));
|
|
2109
|
+
}
|
|
2110
|
+
const verdict = report.ok ? out.green(CHECK) : out.red(CROSS);
|
|
2111
|
+
return `${verdict} ${report.findings.length} checks: ${parts.join(out.dim(" \xB7 "))}`;
|
|
2112
|
+
}
|
|
1683
2113
|
function renderDoctor(report) {
|
|
1684
2114
|
const rows = report.findings.map((finding) => ({
|
|
1685
|
-
glyph: finding.severity
|
|
2115
|
+
glyph: GLYPHS[finding.severity],
|
|
1686
2116
|
label: finding.label,
|
|
1687
2117
|
...finding.subject === void 0 ? {} : { subject: finding.subject },
|
|
1688
2118
|
...finding.detail === void 0 ? {} : { detail: finding.detail }
|
|
1689
2119
|
}));
|
|
1690
|
-
const lines =
|
|
2120
|
+
const lines = [heading("penv doctor", `environment ${report.environment}`), ""];
|
|
2121
|
+
lines.push(...formatRows(rows));
|
|
1691
2122
|
const remedies = [
|
|
1692
2123
|
...new Set(
|
|
1693
2124
|
report.findings.filter((finding) => finding.severity !== "pass").map((finding) => finding.remedy).filter((remedy) => remedy !== void 0)
|
|
1694
2125
|
)
|
|
1695
2126
|
];
|
|
1696
|
-
|
|
1697
|
-
lines.push(
|
|
2127
|
+
if (remedies.length > 0) {
|
|
2128
|
+
lines.push("");
|
|
2129
|
+
for (const remedy of remedies) {
|
|
2130
|
+
lines.push(tip(remedy));
|
|
2131
|
+
}
|
|
1698
2132
|
}
|
|
2133
|
+
lines.push("", summarize(report));
|
|
1699
2134
|
return lines;
|
|
1700
2135
|
}
|
|
1701
2136
|
var doctorCommand = (0, import_citty3.defineCommand)({
|
|
1702
2137
|
meta: {
|
|
1703
2138
|
name: "doctor",
|
|
1704
|
-
description: "Report missing, weak, unused, fallback, plaintext-secret, and
|
|
2139
|
+
description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
|
|
1705
2140
|
},
|
|
1706
2141
|
args: {
|
|
1707
2142
|
env: { type: "string", description: "The environment to report on" }
|
|
@@ -1710,7 +2145,8 @@ var doctorCommand = (0, import_citty3.defineCommand)({
|
|
|
1710
2145
|
return guard(async () => {
|
|
1711
2146
|
const report = await runDoctor({
|
|
1712
2147
|
cwd: process.cwd(),
|
|
1713
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
2148
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2149
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1714
2150
|
});
|
|
1715
2151
|
write(renderDoctor(report));
|
|
1716
2152
|
if (!report.ok) {
|
|
@@ -1721,11 +2157,11 @@ var doctorCommand = (0, import_citty3.defineCommand)({
|
|
|
1721
2157
|
});
|
|
1722
2158
|
|
|
1723
2159
|
// src/commands/encrypt.ts
|
|
1724
|
-
var
|
|
2160
|
+
var import_core10 = require("@penvhq/core");
|
|
1725
2161
|
var import_citty5 = require("citty");
|
|
1726
2162
|
|
|
1727
2163
|
// src/commands/set.ts
|
|
1728
|
-
var
|
|
2164
|
+
var import_core9 = require("@penvhq/core");
|
|
1729
2165
|
var import_citty4 = require("citty");
|
|
1730
2166
|
function scopeFrom(options) {
|
|
1731
2167
|
if (options.local === true) {
|
|
@@ -1745,7 +2181,7 @@ function targetScope(project, options, key) {
|
|
|
1745
2181
|
return scopeFrom(options);
|
|
1746
2182
|
}
|
|
1747
2183
|
if (environment.trim().length === 0) {
|
|
1748
|
-
throw new
|
|
2184
|
+
throw new import_core9.PenvError(
|
|
1749
2185
|
"ENVIRONMENT_FLAG_EMPTY",
|
|
1750
2186
|
`\`--env\` for parameter ${key} names no environment`,
|
|
1751
2187
|
`Pass a declared environment \u2014 ${project.config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
|
|
@@ -1758,27 +2194,27 @@ function policyEnvironment(project, options) {
|
|
|
1758
2194
|
}
|
|
1759
2195
|
function sealFor(project, file, value, parameter, environment) {
|
|
1760
2196
|
if (environment === void 0) {
|
|
1761
|
-
throw new
|
|
2197
|
+
throw new import_core9.PenvError(
|
|
1762
2198
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1763
|
-
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0,
|
|
2199
|
+
`Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0, import_core9.formatValueFile)(file)} names no environment`,
|
|
1764
2200
|
"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."
|
|
1765
2201
|
);
|
|
1766
2202
|
}
|
|
1767
|
-
return (0,
|
|
2203
|
+
return (0, import_core9.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1768
2204
|
}
|
|
1769
2205
|
async function sealAwareWrite(options) {
|
|
1770
2206
|
const { project, provider, ref, scope, value, environment } = options;
|
|
1771
|
-
const secret = (0,
|
|
2207
|
+
const secret = (0, import_core9.isSecret)(await provider.readMeta(ref), environment);
|
|
1772
2208
|
const file = {
|
|
1773
2209
|
namespace: ref.namespace,
|
|
1774
2210
|
name: ref.name,
|
|
1775
2211
|
scope,
|
|
1776
2212
|
encrypted: secret
|
|
1777
2213
|
};
|
|
1778
|
-
const stored = secret ? sealFor(project, file, value, (0,
|
|
2214
|
+
const stored = secret ? sealFor(project, file, value, (0, import_core9.parameterId)(ref), environment) : value;
|
|
1779
2215
|
await provider.write(file, stored);
|
|
1780
2216
|
await provider.remove({ ...file, encrypted: !secret });
|
|
1781
|
-
return { encrypted: secret, location: (0,
|
|
2217
|
+
return { encrypted: secret, location: (0, import_core9.formatValueFile)(file) };
|
|
1782
2218
|
}
|
|
1783
2219
|
async function runSet(options) {
|
|
1784
2220
|
const project = openProject(options.cwd);
|
|
@@ -1860,7 +2296,7 @@ function twins(project, key, options) {
|
|
|
1860
2296
|
}
|
|
1861
2297
|
function environmentFor(project, options, verb) {
|
|
1862
2298
|
if (options.environment === void 0) {
|
|
1863
|
-
throw new
|
|
2299
|
+
throw new import_core10.PenvError(
|
|
1864
2300
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1865
2301
|
`\`penv ${verb}\` names no environment, and keys are declared per environment`,
|
|
1866
2302
|
"Pass `--env <environment>`. penv cannot tell which environment's key applies to a file that names none, and will not pick one for you."
|
|
@@ -1875,32 +2311,32 @@ async function runEncrypt(options) {
|
|
|
1875
2311
|
const project = openProject(options.cwd);
|
|
1876
2312
|
const environment = environmentFor(project, options, "encrypt");
|
|
1877
2313
|
const [plain, sealed] = twins(project, options.key, options);
|
|
1878
|
-
const parameter = (0,
|
|
2314
|
+
const parameter = (0, import_core10.parameterId)(plain);
|
|
1879
2315
|
const value = await readOne(project, plain);
|
|
1880
2316
|
if (value === void 0) {
|
|
1881
2317
|
const already = await readOne(project, sealed);
|
|
1882
|
-
throw new
|
|
2318
|
+
throw new import_core10.PenvError(
|
|
1883
2319
|
"PARAMETER_ABSENT",
|
|
1884
|
-
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0,
|
|
2320
|
+
already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
|
|
1885
2321
|
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."
|
|
1886
2322
|
);
|
|
1887
2323
|
}
|
|
1888
|
-
const text = (0,
|
|
2324
|
+
const text = (0, import_core10.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
|
|
1889
2325
|
await project.provider.write(sealed, text);
|
|
1890
2326
|
await project.provider.remove(plain);
|
|
1891
2327
|
return {
|
|
1892
2328
|
parameter,
|
|
1893
|
-
location: (0,
|
|
1894
|
-
removed: (0,
|
|
2329
|
+
location: (0, import_core10.formatValueFile)(sealed),
|
|
2330
|
+
removed: (0, import_core10.formatValueFile)(plain)
|
|
1895
2331
|
};
|
|
1896
2332
|
}
|
|
1897
2333
|
async function runDecrypt(options) {
|
|
1898
2334
|
const project = openProject(options.cwd);
|
|
1899
2335
|
const environment = environmentFor(project, options, "decrypt");
|
|
1900
2336
|
const [plain, sealed] = twins(project, options.key, options);
|
|
1901
|
-
const parameter = (0,
|
|
1902
|
-
if ((0,
|
|
1903
|
-
throw new
|
|
2337
|
+
const parameter = (0, import_core10.parameterId)(plain);
|
|
2338
|
+
if ((0, import_core10.isSecret)(await project.provider.readMeta(plain), environment)) {
|
|
2339
|
+
throw new import_core10.PenvError(
|
|
1904
2340
|
"SECRET_DECRYPT_REFUSED",
|
|
1905
2341
|
`Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
|
|
1906
2342
|
"A secret with a plaintext value file is a `penv doctor` failure. Drop `secret` from the parameter's meta if it is not one, or run `penv generate --allow-decrypt` if you need the plaintext value in a `.env` artifact."
|
|
@@ -1908,18 +2344,18 @@ async function runDecrypt(options) {
|
|
|
1908
2344
|
}
|
|
1909
2345
|
const stored = await readOne(project, sealed);
|
|
1910
2346
|
if (stored === void 0) {
|
|
1911
|
-
throw new
|
|
2347
|
+
throw new import_core10.PenvError(
|
|
1912
2348
|
"PARAMETER_ABSENT",
|
|
1913
|
-
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0,
|
|
2349
|
+
`Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
|
|
1914
2350
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
1915
2351
|
);
|
|
1916
2352
|
}
|
|
1917
|
-
const opened = (0,
|
|
2353
|
+
const opened = (0, import_core10.openValue)(sealed, stored, keySourceFor(project, environment));
|
|
1918
2354
|
if (opened.kind === "failed") {
|
|
1919
2355
|
throw new UndecryptableAt(
|
|
1920
2356
|
parameter,
|
|
1921
2357
|
environment,
|
|
1922
|
-
(0,
|
|
2358
|
+
(0, import_core10.formatValueFile)(sealed),
|
|
1923
2359
|
opened.failure.detail
|
|
1924
2360
|
);
|
|
1925
2361
|
}
|
|
@@ -1927,11 +2363,11 @@ async function runDecrypt(options) {
|
|
|
1927
2363
|
await project.provider.remove(sealed);
|
|
1928
2364
|
return {
|
|
1929
2365
|
parameter,
|
|
1930
|
-
location: (0,
|
|
1931
|
-
removed: (0,
|
|
2366
|
+
location: (0, import_core10.formatValueFile)(plain),
|
|
2367
|
+
removed: (0, import_core10.formatValueFile)(sealed)
|
|
1932
2368
|
};
|
|
1933
2369
|
}
|
|
1934
|
-
var UndecryptableAt = class extends
|
|
2370
|
+
var UndecryptableAt = class extends import_core10.PenvError {
|
|
1935
2371
|
constructor(parameter, environment, location, detail) {
|
|
1936
2372
|
super(
|
|
1937
2373
|
"VALUE_UNDECRYPTABLE",
|
|
@@ -1986,14 +2422,14 @@ var decryptCommand = (0, import_citty5.defineCommand)({
|
|
|
1986
2422
|
});
|
|
1987
2423
|
|
|
1988
2424
|
// src/commands/fill.ts
|
|
1989
|
-
var
|
|
1990
|
-
var import_core10 = require("@penvhq/core");
|
|
2425
|
+
var import_core11 = require("@penvhq/core");
|
|
1991
2426
|
var import_citty6 = require("citty");
|
|
1992
2427
|
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
1993
2428
|
async function runFill(options) {
|
|
1994
2429
|
const validation = await runValidate({
|
|
1995
2430
|
cwd: options.cwd,
|
|
1996
|
-
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
2431
|
+
...options.environment === void 0 ? {} : { environment: options.environment },
|
|
2432
|
+
...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
|
|
1997
2433
|
});
|
|
1998
2434
|
const environment = validation.environment;
|
|
1999
2435
|
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
@@ -2001,7 +2437,7 @@ async function runFill(options) {
|
|
|
2001
2437
|
const detail = blockers.map(
|
|
2002
2438
|
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
2003
2439
|
).join("\n");
|
|
2004
|
-
throw new
|
|
2440
|
+
throw new import_core11.PenvError(
|
|
2005
2441
|
"FILL_BLOCKED",
|
|
2006
2442
|
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
2007
2443
|
${detail}`,
|
|
@@ -2010,6 +2446,7 @@ ${detail}`,
|
|
|
2010
2446
|
}
|
|
2011
2447
|
const written = [];
|
|
2012
2448
|
const skipped2 = [];
|
|
2449
|
+
const kept = [];
|
|
2013
2450
|
const unreachable = [];
|
|
2014
2451
|
for (const drift of validation.drift.declared) {
|
|
2015
2452
|
if (drift.ref === void 0) {
|
|
@@ -2018,7 +2455,12 @@ ${detail}`,
|
|
|
2018
2455
|
}
|
|
2019
2456
|
const ref = drift.ref;
|
|
2020
2457
|
const key = [...ref.namespace, ref.name].join("/");
|
|
2021
|
-
const value = await options.ask({
|
|
2458
|
+
const value = await options.ask({
|
|
2459
|
+
parameter: key,
|
|
2460
|
+
environment,
|
|
2461
|
+
secret: false,
|
|
2462
|
+
optional: false
|
|
2463
|
+
});
|
|
2022
2464
|
if (value === void 0 || value === "") {
|
|
2023
2465
|
skipped2.push(drift.subject);
|
|
2024
2466
|
continue;
|
|
@@ -2030,17 +2472,45 @@ ${detail}`,
|
|
|
2030
2472
|
encrypted: result2.encrypted
|
|
2031
2473
|
});
|
|
2032
2474
|
}
|
|
2033
|
-
|
|
2475
|
+
for (const item of validation.drift.optional) {
|
|
2476
|
+
if (item.ref === void 0) {
|
|
2477
|
+
unreachable.push({ subject: item.subject, remedy: item.remedy });
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
const ref = item.ref;
|
|
2481
|
+
const key = [...ref.namespace, ref.name].join("/");
|
|
2482
|
+
const value = await options.ask({
|
|
2483
|
+
parameter: key,
|
|
2484
|
+
environment,
|
|
2485
|
+
secret: false,
|
|
2486
|
+
optional: true,
|
|
2487
|
+
...item.defaultValue === void 0 ? {} : { defaultValue: item.defaultValue }
|
|
2488
|
+
});
|
|
2489
|
+
if (value === void 0 || value === "") {
|
|
2490
|
+
kept.push(item.subject);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const result2 = await runSet({ cwd: options.cwd, key, value, environment });
|
|
2494
|
+
written.push({
|
|
2495
|
+
parameter: item.subject,
|
|
2496
|
+
location: result2.location,
|
|
2497
|
+
encrypted: result2.encrypted
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
return { environment, written, skipped: skipped2, kept, unreachable };
|
|
2034
2501
|
}
|
|
2035
2502
|
function summaryLine(result2) {
|
|
2036
2503
|
const filled = result2.written.length;
|
|
2037
|
-
if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
|
|
2038
|
-
return
|
|
2504
|
+
if (filled === 0 && result2.skipped.length === 0 && result2.kept.length === 0 && result2.unreachable.length === 0) {
|
|
2505
|
+
return `${out.green(CHECK)} Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
|
|
2039
2506
|
}
|
|
2040
2507
|
const parts = [`${filled} written`];
|
|
2041
2508
|
if (result2.skipped.length > 0) {
|
|
2042
2509
|
parts.push(`${result2.skipped.length} skipped`);
|
|
2043
2510
|
}
|
|
2511
|
+
if (result2.kept.length > 0) {
|
|
2512
|
+
parts.push(`${result2.kept.length} left to the schema's defaults`);
|
|
2513
|
+
}
|
|
2044
2514
|
if (result2.unreachable.length > 0) {
|
|
2045
2515
|
parts.push(`${result2.unreachable.length} unreachable`);
|
|
2046
2516
|
}
|
|
@@ -2061,6 +2531,16 @@ function renderFill(result2) {
|
|
|
2061
2531
|
lines.push(summaryLine(result2));
|
|
2062
2532
|
return lines;
|
|
2063
2533
|
}
|
|
2534
|
+
function contextFor(prompt2) {
|
|
2535
|
+
if (!prompt2.optional) {
|
|
2536
|
+
return `${prompt2.environment}, Enter to skip`;
|
|
2537
|
+
}
|
|
2538
|
+
if (prompt2.defaultValue === void 0) {
|
|
2539
|
+
return `${prompt2.environment} \xB7 optional, Enter leaves it unset`;
|
|
2540
|
+
}
|
|
2541
|
+
const shown = prompt2.defaultValue.length > 24 ? `${prompt2.defaultValue.slice(0, 23)}\u2026` : prompt2.defaultValue;
|
|
2542
|
+
return `${prompt2.environment} \xB7 optional, Enter keeps ${shown}`;
|
|
2543
|
+
}
|
|
2064
2544
|
var fillCommand = (0, import_citty6.defineCommand)({
|
|
2065
2545
|
meta: {
|
|
2066
2546
|
name: "fill",
|
|
@@ -2071,20 +2551,21 @@ var fillCommand = (0, import_citty6.defineCommand)({
|
|
|
2071
2551
|
},
|
|
2072
2552
|
run({ args }) {
|
|
2073
2553
|
return guard(async () => {
|
|
2074
|
-
const
|
|
2075
|
-
const ask = (
|
|
2554
|
+
const reader = lineReader();
|
|
2555
|
+
const ask = (prompt2) => reader.ask(prompt(prompt2.parameter, contextFor(prompt2)));
|
|
2076
2556
|
try {
|
|
2077
2557
|
write(
|
|
2078
2558
|
renderFill(
|
|
2079
2559
|
await runFill({
|
|
2080
2560
|
cwd: process.cwd(),
|
|
2081
2561
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2082
|
-
ask
|
|
2562
|
+
ask,
|
|
2563
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
2083
2564
|
})
|
|
2084
2565
|
)
|
|
2085
2566
|
);
|
|
2086
2567
|
} finally {
|
|
2087
|
-
|
|
2568
|
+
reader.close();
|
|
2088
2569
|
}
|
|
2089
2570
|
});
|
|
2090
2571
|
}
|
|
@@ -2093,14 +2574,14 @@ var fillCommand = (0, import_citty6.defineCommand)({
|
|
|
2093
2574
|
// src/commands/generate.ts
|
|
2094
2575
|
var import_node_fs = require("fs");
|
|
2095
2576
|
var import_node_path4 = require("path");
|
|
2096
|
-
var
|
|
2577
|
+
var import_core12 = require("@penvhq/core");
|
|
2097
2578
|
var import_citty7 = require("citty");
|
|
2098
2579
|
var DEFAULT_OUTPUT = ".env";
|
|
2099
2580
|
function entriesFor(project, environment, allowDecrypt) {
|
|
2100
2581
|
const keys = keySourceFor(project, environment);
|
|
2101
2582
|
const tree = localTree(project);
|
|
2102
2583
|
const resolutions = resolveAllSync(tree, environment, keys);
|
|
2103
|
-
const collision = (0,
|
|
2584
|
+
const collision = (0, import_core12.checkNameCollisions)(
|
|
2104
2585
|
refsFrom(resolutions.map((resolution) => resolution.ref)),
|
|
2105
2586
|
project.config
|
|
2106
2587
|
)[0];
|
|
@@ -2113,21 +2594,21 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2113
2594
|
const winner = resolution.winner;
|
|
2114
2595
|
if (winner?.file.encrypted === true) {
|
|
2115
2596
|
if (!allowDecrypt) {
|
|
2116
|
-
throw new
|
|
2597
|
+
throw new import_core12.PenvError(
|
|
2117
2598
|
"ENCRYPTED_VALUE_REFUSED",
|
|
2118
2599
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
2119
2600
|
`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.`
|
|
2120
2601
|
);
|
|
2121
2602
|
}
|
|
2122
|
-
(0,
|
|
2603
|
+
(0, import_core12.requireValue)(resolution, environment);
|
|
2123
2604
|
decrypted += 1;
|
|
2124
2605
|
}
|
|
2125
2606
|
if (resolution.value === void 0) {
|
|
2126
2607
|
continue;
|
|
2127
2608
|
}
|
|
2128
|
-
const description = (0,
|
|
2609
|
+
const description = (0, import_core12.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
|
|
2129
2610
|
entries.push({
|
|
2130
|
-
key: (0,
|
|
2611
|
+
key: (0, import_core12.variableName)(resolution.ref, project.config),
|
|
2131
2612
|
value: resolution.value,
|
|
2132
2613
|
...typeof description === "string" ? { description } : {}
|
|
2133
2614
|
});
|
|
@@ -2137,15 +2618,15 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2137
2618
|
}
|
|
2138
2619
|
function generateDotenv(options) {
|
|
2139
2620
|
const project = openProject(options.cwd);
|
|
2140
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2141
|
-
return (0,
|
|
2621
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2622
|
+
return (0, import_core12.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
|
|
2142
2623
|
}
|
|
2143
2624
|
function runGenerate(options) {
|
|
2144
2625
|
const project = openProject(options.cwd);
|
|
2145
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2626
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2146
2627
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
2147
2628
|
const file = options.out === void 0 ? (0, import_node_path4.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path4.isAbsolute)(options.out) ? options.out : (0, import_node_path4.resolve)(options.cwd, options.out);
|
|
2148
|
-
(0, import_node_fs.writeFileSync)(file, (0,
|
|
2629
|
+
(0, import_node_fs.writeFileSync)(file, (0, import_core12.serializeDotenv)(entries), "utf8");
|
|
2149
2630
|
return { file, environment, entries: entries.length, decrypted };
|
|
2150
2631
|
}
|
|
2151
2632
|
function displayPath(cwd, file) {
|
|
@@ -2188,7 +2669,8 @@ var generateCommand = (0, import_citty7.defineCommand)({
|
|
|
2188
2669
|
cwd,
|
|
2189
2670
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2190
2671
|
...args.out === void 0 ? {} : { out: args.out },
|
|
2191
|
-
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
|
|
2672
|
+
...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
|
|
2673
|
+
envFlags: shorthandCandidates(args, ["env", "out", "allow-decrypt", "allowDecrypt"])
|
|
2192
2674
|
});
|
|
2193
2675
|
write(renderGenerate(result2, cwd));
|
|
2194
2676
|
});
|
|
@@ -2196,17 +2678,17 @@ var generateCommand = (0, import_citty7.defineCommand)({
|
|
|
2196
2678
|
});
|
|
2197
2679
|
|
|
2198
2680
|
// src/commands/get.ts
|
|
2199
|
-
var
|
|
2681
|
+
var import_core13 = require("@penvhq/core");
|
|
2200
2682
|
var import_citty8 = require("citty");
|
|
2201
2683
|
async function runGet(options) {
|
|
2202
2684
|
const project = openProject(options.cwd);
|
|
2203
2685
|
const environment = targetEnvironment(project, options.environment);
|
|
2204
2686
|
const ref = refFromKey(options.key);
|
|
2205
2687
|
const keys = keySourceFor(project, environment);
|
|
2206
|
-
const resolution = await (0,
|
|
2207
|
-
const value = (0,
|
|
2688
|
+
const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
|
|
2689
|
+
const value = (0, import_core13.requireValue)(resolution, environment);
|
|
2208
2690
|
if (value === void 0) {
|
|
2209
|
-
throw new
|
|
2691
|
+
throw new import_core13.PenvError(
|
|
2210
2692
|
"PARAMETER_ABSENT",
|
|
2211
2693
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
2212
2694
|
`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.`
|
|
@@ -2228,7 +2710,7 @@ async function runExplain(options) {
|
|
|
2228
2710
|
const environment = targetEnvironment(project, options.environment);
|
|
2229
2711
|
const ref = refFromKey(options.key);
|
|
2230
2712
|
const keys = keySourceFor(project, environment);
|
|
2231
|
-
const resolution = await (0,
|
|
2713
|
+
const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
|
|
2232
2714
|
const winner = resolution.winner;
|
|
2233
2715
|
return {
|
|
2234
2716
|
parameter: resolution.parameter,
|
|
@@ -2246,12 +2728,14 @@ async function runExplain(options) {
|
|
|
2246
2728
|
function renderExplain(explanation) {
|
|
2247
2729
|
const target = explanation.location === void 0 ? "nothing" : `${PENV_DIR}/${explanation.location}`;
|
|
2248
2730
|
const rows = explanation.candidates.map((candidate) => [
|
|
2249
|
-
candidate.location,
|
|
2250
|
-
candidate.wins ? "present, wins" :
|
|
2731
|
+
candidate.wins ? candidate.location : out.dim(candidate.location),
|
|
2732
|
+
candidate.wins ? out.green("present, wins") : out.dim(
|
|
2733
|
+
candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
|
|
2734
|
+
)
|
|
2251
2735
|
]);
|
|
2252
2736
|
return [
|
|
2253
|
-
`${explanation.parameter} resolves to ${target} for environment ${explanation.environment}`,
|
|
2254
|
-
...explanation.undecryptable === void 0 ? [] : [` penv cannot decrypt it: ${explanation.undecryptable}`],
|
|
2737
|
+
`${out.bold(explanation.parameter)} resolves to ${target} for environment ${explanation.environment}`,
|
|
2738
|
+
...explanation.undecryptable === void 0 ? [] : [` ${out.red(CROSS)} penv cannot decrypt it: ${explanation.undecryptable}`],
|
|
2255
2739
|
"",
|
|
2256
2740
|
...columns(rows).map((line) => ` ${line}`)
|
|
2257
2741
|
];
|
|
@@ -2282,13 +2766,13 @@ var getCommand = (0, import_citty8.defineCommand)({
|
|
|
2282
2766
|
// src/commands/import.ts
|
|
2283
2767
|
var import_node_fs4 = require("fs");
|
|
2284
2768
|
var import_node_path7 = require("path");
|
|
2285
|
-
var
|
|
2769
|
+
var import_core16 = require("@penvhq/core");
|
|
2286
2770
|
var import_citty10 = require("citty");
|
|
2287
2771
|
|
|
2288
2772
|
// src/detect.ts
|
|
2289
2773
|
var import_node_fs2 = require("fs");
|
|
2290
2774
|
var import_node_path5 = require("path");
|
|
2291
|
-
var
|
|
2775
|
+
var import_core14 = require("@penvhq/core");
|
|
2292
2776
|
var SIGNATURES = [
|
|
2293
2777
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
2294
2778
|
{
|
|
@@ -2326,7 +2810,7 @@ function schemaFileFor(cwd) {
|
|
|
2326
2810
|
if (!occupied(cwd, beside)) {
|
|
2327
2811
|
return { file: beside, displaced: preferred };
|
|
2328
2812
|
}
|
|
2329
|
-
return { file:
|
|
2813
|
+
return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2330
2814
|
}
|
|
2331
2815
|
function dependenciesOf(cwd) {
|
|
2332
2816
|
const manifest = manifestOf(cwd);
|
|
@@ -2389,8 +2873,8 @@ function hasImportsBlock(cwd) {
|
|
|
2389
2873
|
// src/commands/init.ts
|
|
2390
2874
|
var import_node_fs3 = require("fs");
|
|
2391
2875
|
var import_node_path6 = require("path");
|
|
2392
|
-
var
|
|
2393
|
-
var
|
|
2876
|
+
var import_promises = require("readline/promises");
|
|
2877
|
+
var import_core15 = require("@penvhq/core");
|
|
2394
2878
|
var import_citty9 = require("citty");
|
|
2395
2879
|
var CONFIG_FILE = "penv.config.ts";
|
|
2396
2880
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
@@ -2401,11 +2885,11 @@ var IMPORTS_PREFIX = "#";
|
|
|
2401
2885
|
var PACKAGE_FILE = "package.json";
|
|
2402
2886
|
var DEFAULT_DECISIONS = {
|
|
2403
2887
|
environments: [],
|
|
2404
|
-
schemaFile:
|
|
2888
|
+
schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
|
|
2405
2889
|
publicPrefixes: [],
|
|
2406
2890
|
alias: DEFAULT_ALIAS
|
|
2407
2891
|
};
|
|
2408
|
-
var NOT_ENVIRONMENTS = [...
|
|
2892
|
+
var NOT_ENVIRONMENTS = [...import_core15.RESERVED_TOKENS, "example", "sample", "template"];
|
|
2409
2893
|
function suggestEnvironments(root) {
|
|
2410
2894
|
let entries;
|
|
2411
2895
|
try {
|
|
@@ -2421,7 +2905,7 @@ function suggestEnvironments(root) {
|
|
|
2421
2905
|
const segments = entry.slice(".env.".length).split(".");
|
|
2422
2906
|
const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
|
|
2423
2907
|
const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
|
|
2424
|
-
if (name === void 0 || !(0,
|
|
2908
|
+
if (name === void 0 || !(0, import_core15.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
|
|
2425
2909
|
continue;
|
|
2426
2910
|
}
|
|
2427
2911
|
found.add(name);
|
|
@@ -2429,10 +2913,10 @@ function suggestEnvironments(root) {
|
|
|
2429
2913
|
return [...found].sort();
|
|
2430
2914
|
}
|
|
2431
2915
|
function emptyFlag(flag) {
|
|
2432
|
-
return new
|
|
2916
|
+
return new import_core15.PenvError(
|
|
2433
2917
|
"INIT_FLAG_EMPTY",
|
|
2434
2918
|
`\`--${flag}\` was given without a value`,
|
|
2435
|
-
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${
|
|
2919
|
+
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core15.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
|
|
2436
2920
|
);
|
|
2437
2921
|
}
|
|
2438
2922
|
function splitEnvironments(value) {
|
|
@@ -2457,7 +2941,7 @@ function declaredIn(root) {
|
|
|
2457
2941
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
2458
2942
|
return void 0;
|
|
2459
2943
|
}
|
|
2460
|
-
return (0,
|
|
2944
|
+
return (0, import_core15.loadConfigFrom)(file);
|
|
2461
2945
|
}
|
|
2462
2946
|
function planInit(root, flags = {}) {
|
|
2463
2947
|
const declared = declaredIn(root);
|
|
@@ -2467,7 +2951,7 @@ function planInit(root, flags = {}) {
|
|
|
2467
2951
|
notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
|
|
2468
2952
|
} else if (detected === void 0) {
|
|
2469
2953
|
notes.push(
|
|
2470
|
-
`No framework detected in package.json \u2014 the schema goes to ${
|
|
2954
|
+
`No framework detected in package.json \u2014 the schema goes to ${import_core15.DEFAULT_SCHEMA_FILE}.`
|
|
2471
2955
|
);
|
|
2472
2956
|
} else {
|
|
2473
2957
|
notes.push(`Detected ${detected.name}.`);
|
|
@@ -2477,12 +2961,12 @@ function planInit(root, flags = {}) {
|
|
|
2477
2961
|
);
|
|
2478
2962
|
}
|
|
2479
2963
|
}
|
|
2480
|
-
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0,
|
|
2964
|
+
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core15.schemaFileOf)(declared) : detected?.schemaFile ?? import_core15.DEFAULT_SCHEMA_FILE : flags.schema.trim();
|
|
2481
2965
|
if (flags.schema !== void 0) {
|
|
2482
2966
|
if (schemaFile.length === 0) {
|
|
2483
2967
|
throw emptyFlag("schema");
|
|
2484
2968
|
}
|
|
2485
|
-
const error = (0,
|
|
2969
|
+
const error = (0, import_core15.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
|
|
2486
2970
|
if (error !== void 0) {
|
|
2487
2971
|
throw error;
|
|
2488
2972
|
}
|
|
@@ -2492,7 +2976,7 @@ function planInit(root, flags = {}) {
|
|
|
2492
2976
|
throw emptyFlag("alias");
|
|
2493
2977
|
}
|
|
2494
2978
|
if (!ALIAS_NAME.test(alias)) {
|
|
2495
|
-
throw new
|
|
2979
|
+
throw new import_core15.PenvError(
|
|
2496
2980
|
"INIT_ALIAS_INVALID",
|
|
2497
2981
|
`\`${alias}\` is not an alias penv can write`,
|
|
2498
2982
|
`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.`
|
|
@@ -2530,23 +3014,27 @@ function planInit(root, flags = {}) {
|
|
|
2530
3014
|
function renderPlan(plan2) {
|
|
2531
3015
|
const rows = [];
|
|
2532
3016
|
rows.push([
|
|
2533
|
-
"
|
|
2534
|
-
plan2.suggestedEnvironments.length === 0 ? "" : `[${plan2.suggestedEnvironments.join(", ")}]
|
|
2535
|
-
|
|
3017
|
+
` ${out.dim("environments")}`,
|
|
3018
|
+
plan2.suggestedEnvironments.length === 0 ? "" : out.cyan(`[${plan2.suggestedEnvironments.join(", ")}]`),
|
|
3019
|
+
out.dim(
|
|
3020
|
+
plan2.suggestedEnvironments.length === 0 ? "\u2190 name them, or Enter to leave the whitelist empty" : "\u2190 from your .env files; edit, or Enter to accept"
|
|
3021
|
+
)
|
|
2536
3022
|
]);
|
|
2537
3023
|
rows.push([
|
|
2538
|
-
"
|
|
3024
|
+
` ${out.dim("schemaFile")}`,
|
|
2539
3025
|
plan2.decisions.schemaFile,
|
|
2540
|
-
plan2.decisions.schemaFile ===
|
|
3026
|
+
plan2.decisions.schemaFile === import_core15.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core15.DEFAULT_SCHEMA_FILE})`)
|
|
2541
3027
|
]);
|
|
2542
3028
|
for (const prefix of plan2.decisions.publicPrefixes) {
|
|
2543
|
-
rows.push(["
|
|
3029
|
+
rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
|
|
2544
3030
|
}
|
|
2545
|
-
const headline =
|
|
3031
|
+
const headline = out.bold(
|
|
3032
|
+
plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`
|
|
3033
|
+
);
|
|
2546
3034
|
return [headline, "", ...columns(rows), ""];
|
|
2547
3035
|
}
|
|
2548
3036
|
function environmentsHint(plan2) {
|
|
2549
|
-
return plan2.suggestedEnvironments.length === 0 ? "environments
|
|
3037
|
+
return plan2.suggestedEnvironments.length === 0 ? prompt("environments", "comma-separated, Enter for none") : prompt("environments", 'Enter to accept, "none" for an empty whitelist');
|
|
2550
3038
|
}
|
|
2551
3039
|
async function promptForDecisions(plan2, io) {
|
|
2552
3040
|
for (const line of renderPlan(plan2)) {
|
|
@@ -2561,7 +3049,7 @@ async function promptForDecisions(plan2, io) {
|
|
|
2561
3049
|
);
|
|
2562
3050
|
io.write("");
|
|
2563
3051
|
}
|
|
2564
|
-
const proceed = (await io.ask("Proceed?
|
|
3052
|
+
const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
|
|
2565
3053
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
2566
3054
|
return void 0;
|
|
2567
3055
|
}
|
|
@@ -2584,6 +3072,15 @@ ${body}
|
|
|
2584
3072
|
// code. Importing it loads configuration and throws (naming the parameter and
|
|
2585
3073
|
// environment) if anything required is missing or invalid.
|
|
2586
3074
|
export const env = load(schema);
|
|
3075
|
+
|
|
3076
|
+
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3077
|
+
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3078
|
+
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
3079
|
+
declare module "@penvhq/core" {
|
|
3080
|
+
interface PenvSchemaShape {
|
|
3081
|
+
readonly shape: z.infer<typeof schema>;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
2587
3084
|
`;
|
|
2588
3085
|
}
|
|
2589
3086
|
function renderEnvironments(decisions) {
|
|
@@ -2593,14 +3090,14 @@ function renderEnvironments(decisions) {
|
|
|
2593
3090
|
// can read off your codebase, and an environment you do not have is worse
|
|
2594
3091
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
2595
3092
|
// environments: ["development", "production"],
|
|
2596
|
-
// providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
|
|
3093
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
|
|
2597
3094
|
environments: [],
|
|
2598
3095
|
|
|
2599
3096
|
providers: {},
|
|
2600
3097
|
`;
|
|
2601
3098
|
}
|
|
2602
3099
|
const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
|
|
2603
|
-
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "filesystem" },
|
|
3100
|
+
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
|
|
2604
3101
|
`).join("");
|
|
2605
3102
|
return `${shared} environments: [${names}],
|
|
2606
3103
|
|
|
@@ -2611,7 +3108,7 @@ ${providers} },
|
|
|
2611
3108
|
}
|
|
2612
3109
|
function renderConfigModule(decisions) {
|
|
2613
3110
|
let body = renderEnvironments(decisions);
|
|
2614
|
-
if (decisions.schemaFile !==
|
|
3111
|
+
if (decisions.schemaFile !== import_core15.DEFAULT_SCHEMA_FILE) {
|
|
2615
3112
|
body += `
|
|
2616
3113
|
// The module that exports the schema. It is yours \u2014 penv scaffolds it once
|
|
2617
3114
|
// and never regenerates it \u2014 so this says where you keep it.
|
|
@@ -2634,7 +3131,7 @@ ${body}});
|
|
|
2634
3131
|
`;
|
|
2635
3132
|
}
|
|
2636
3133
|
function renderGitignore(decisions) {
|
|
2637
|
-
const inside = (0,
|
|
3134
|
+
const inside = (0, import_core15.schemaInsideTree)(configOf(decisions));
|
|
2638
3135
|
const listed = inside === void 0 ? "" : `${inside}, `;
|
|
2639
3136
|
return `# Written by penv. Value files hold configuration values and are never
|
|
2640
3137
|
# committed; only the structure, ${listed}meta, and config are.
|
|
@@ -2779,13 +3276,13 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
2779
3276
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
2780
3277
|
}
|
|
2781
3278
|
function shapeError(what, target, alias) {
|
|
2782
|
-
return new
|
|
3279
|
+
return new import_core15.PenvError(
|
|
2783
3280
|
"TSCONFIG_SHAPE",
|
|
2784
3281
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
2785
3282
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
2786
3283
|
);
|
|
2787
3284
|
}
|
|
2788
|
-
function insertEnvAlias(source, target =
|
|
3285
|
+
function insertEnvAlias(source, target = import_core15.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
|
|
2789
3286
|
const alias = `"${name}": ["${target}"]`;
|
|
2790
3287
|
const root = skipTrivia(source, 0);
|
|
2791
3288
|
if (source.charAt(root) !== "{") {
|
|
@@ -2966,14 +3463,15 @@ function renderInit(result2) {
|
|
|
2966
3463
|
return [
|
|
2967
3464
|
...formatSteps(steps),
|
|
2968
3465
|
"",
|
|
2969
|
-
|
|
3466
|
+
`${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${result2.decisions.schemaFile}, then:`,
|
|
3467
|
+
tip(out.cyan("penv set <key>")),
|
|
2970
3468
|
...result2.decisions.environments.length === 0 ? [
|
|
2971
3469
|
`Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
|
|
2972
3470
|
] : []
|
|
2973
3471
|
];
|
|
2974
3472
|
}
|
|
2975
3473
|
async function askOnTty(plan2) {
|
|
2976
|
-
const rl = (0,
|
|
3474
|
+
const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2977
3475
|
try {
|
|
2978
3476
|
return await promptForDecisions(plan2, {
|
|
2979
3477
|
ask: (question) => rl.question(question),
|
|
@@ -2993,7 +3491,7 @@ var initCommand = (0, import_citty9.defineCommand)({
|
|
|
2993
3491
|
},
|
|
2994
3492
|
schema: {
|
|
2995
3493
|
type: "string",
|
|
2996
|
-
description: `Where the schema module goes, e.g. src/env.ts (default: ${
|
|
3494
|
+
description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core15.DEFAULT_SCHEMA_FILE})`
|
|
2997
3495
|
},
|
|
2998
3496
|
alias: {
|
|
2999
3497
|
type: "string",
|
|
@@ -3047,7 +3545,7 @@ function inferType(value) {
|
|
|
3047
3545
|
}
|
|
3048
3546
|
function draftFields(entries) {
|
|
3049
3547
|
return entries.map((entry) => {
|
|
3050
|
-
const key = (0,
|
|
3548
|
+
const key = (0, import_core16.accessPath)((0, import_core16.refFromVariable)(entry.key)).join(".");
|
|
3051
3549
|
return {
|
|
3052
3550
|
key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
|
|
3053
3551
|
type: inferType(entry.value)
|
|
@@ -3058,33 +3556,33 @@ function assertImportable(ref, variable) {
|
|
|
3058
3556
|
if (!ref.name.includes(".")) {
|
|
3059
3557
|
return;
|
|
3060
3558
|
}
|
|
3061
|
-
throw new
|
|
3559
|
+
throw new import_core16.PenvError(
|
|
3062
3560
|
"IMPORT_UNPARSEABLE_NAME",
|
|
3063
3561
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
3064
3562
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
3065
3563
|
);
|
|
3066
3564
|
}
|
|
3067
3565
|
function assertNotReserved(ref, variable, where, config) {
|
|
3068
|
-
if ((0,
|
|
3069
|
-
throw new
|
|
3566
|
+
if ((0, import_core16.isReservedToken)(ref.name, config)) {
|
|
3567
|
+
throw new import_core16.ReservedTokenError("parameter", variable, where);
|
|
3070
3568
|
}
|
|
3071
3569
|
}
|
|
3072
3570
|
function assertRoundTrips(ref, variable, config) {
|
|
3073
|
-
if ((0,
|
|
3571
|
+
if ((0, import_core16.roundTripsCleanly)(variable)) {
|
|
3074
3572
|
return;
|
|
3075
3573
|
}
|
|
3076
|
-
if ((0,
|
|
3574
|
+
if ((0, import_core16.variableName)(ref, config) === variable) {
|
|
3077
3575
|
return;
|
|
3078
3576
|
}
|
|
3079
|
-
const generated = (0,
|
|
3080
|
-
throw new
|
|
3577
|
+
const generated = (0, import_core16.variableName)(ref, config);
|
|
3578
|
+
throw new import_core16.PenvError(
|
|
3081
3579
|
"IMPORT_LOSSY_NAME",
|
|
3082
3580
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
3083
|
-
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`
|
|
3581
|
+
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`override\` block of penv.config.ts \u2014 \`override: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
|
|
3084
3582
|
);
|
|
3085
3583
|
}
|
|
3086
3584
|
function collisionsIn(refs, config) {
|
|
3087
|
-
const errors = (0,
|
|
3585
|
+
const errors = (0, import_core16.checkNameCollisions)(refs, config);
|
|
3088
3586
|
const first = errors[0];
|
|
3089
3587
|
if (first !== void 0) {
|
|
3090
3588
|
throw first;
|
|
@@ -3094,7 +3592,7 @@ function assertDeclared(segment, config) {
|
|
|
3094
3592
|
if (config.environments.includes(segment)) {
|
|
3095
3593
|
return segment;
|
|
3096
3594
|
}
|
|
3097
|
-
throw new
|
|
3595
|
+
throw new import_core16.UnknownEnvironmentError(segment, config.environments);
|
|
3098
3596
|
}
|
|
3099
3597
|
function scopeFromFilename(file, config) {
|
|
3100
3598
|
const name = (0, import_node_path7.basename)(file);
|
|
@@ -3119,13 +3617,13 @@ function scopeFromFilename(file, config) {
|
|
|
3119
3617
|
return { kind: "environment-local", environment: assertDeclared(first, config) };
|
|
3120
3618
|
}
|
|
3121
3619
|
if (first === LOCAL && rest.length === 2) {
|
|
3122
|
-
throw new
|
|
3620
|
+
throw new import_core16.FilenameGrammarError(
|
|
3123
3621
|
name,
|
|
3124
3622
|
"`local` precedes the environment segment",
|
|
3125
3623
|
`The environment segment always precedes \`local\` \u2014 \`.env.${second}.${LOCAL}\` is the file Next.js and Vite read, and \`.env.${LOCAL}.${second}\` is not a synonym for it. Rename it to \`.env.${second}.${LOCAL}\`, then import it again. Nothing was imported.`
|
|
3126
3624
|
);
|
|
3127
3625
|
}
|
|
3128
|
-
throw new
|
|
3626
|
+
throw new import_core16.FilenameGrammarError(
|
|
3129
3627
|
name,
|
|
3130
3628
|
`\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
|
|
3131
3629
|
"A dotenv file carries exactly one scope: `.env`, `.env.<environment>`, `.env.local`, or `.env.<environment>.local`. Point `penv import` at one of those. Nothing was imported."
|
|
@@ -3140,7 +3638,7 @@ function environmentOf(scope) {
|
|
|
3140
3638
|
case "local":
|
|
3141
3639
|
return void 0;
|
|
3142
3640
|
default:
|
|
3143
|
-
return (0,
|
|
3641
|
+
return (0, import_core16.assertNever)(scope, "scope");
|
|
3144
3642
|
}
|
|
3145
3643
|
}
|
|
3146
3644
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -3153,7 +3651,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
3153
3651
|
case "environment-local":
|
|
3154
3652
|
return scope;
|
|
3155
3653
|
default:
|
|
3156
|
-
return (0,
|
|
3654
|
+
return (0, import_core16.assertNever)(scope, "scope");
|
|
3157
3655
|
}
|
|
3158
3656
|
}
|
|
3159
3657
|
function explicitEnvironment(options, source, config) {
|
|
@@ -3161,7 +3659,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
3161
3659
|
if (value.length > 0) {
|
|
3162
3660
|
return value;
|
|
3163
3661
|
}
|
|
3164
|
-
throw new
|
|
3662
|
+
throw new import_core16.PenvError(
|
|
3165
3663
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
3166
3664
|
`\`--env\` for the import of ${source} names no environment`,
|
|
3167
3665
|
`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.`
|
|
@@ -3171,7 +3669,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
3171
3669
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
3172
3670
|
return;
|
|
3173
3671
|
}
|
|
3174
|
-
throw new
|
|
3672
|
+
throw new import_core16.PenvError(
|
|
3175
3673
|
"IMPORT_ENV_CONFLICT",
|
|
3176
3674
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
3177
3675
|
`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.`
|
|
@@ -3192,15 +3690,15 @@ function environmentNamed(file, explicit) {
|
|
|
3192
3690
|
function decisionsOf(config, cwd) {
|
|
3193
3691
|
return {
|
|
3194
3692
|
environments: config.environments,
|
|
3195
|
-
schemaFile: (0,
|
|
3693
|
+
schemaFile: (0, import_core16.schemaFileOf)(config),
|
|
3196
3694
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3197
3695
|
alias: detectAlias(cwd)
|
|
3198
3696
|
};
|
|
3199
3697
|
}
|
|
3200
3698
|
function configInEffect(cwd, environment) {
|
|
3201
|
-
const existing = (0,
|
|
3699
|
+
const existing = (0, import_core16.findConfigFile)(cwd);
|
|
3202
3700
|
if (existing !== void 0) {
|
|
3203
|
-
const config = (0,
|
|
3701
|
+
const config = (0, import_core16.loadConfigFrom)(existing);
|
|
3204
3702
|
return { config, decisions: decisionsOf(config, cwd) };
|
|
3205
3703
|
}
|
|
3206
3704
|
const planned = planInit(cwd).decisions;
|
|
@@ -3215,13 +3713,13 @@ function importDotenv(options) {
|
|
|
3215
3713
|
const cwd = (0, import_node_path7.resolve)(options.cwd);
|
|
3216
3714
|
const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
|
|
3217
3715
|
if (!(0, import_node_fs4.existsSync)(file)) {
|
|
3218
|
-
throw new
|
|
3716
|
+
throw new import_core16.PenvError(
|
|
3219
3717
|
"IMPORT_FILE_MISSING",
|
|
3220
3718
|
`There is no file at ${file} to import`,
|
|
3221
3719
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
3222
3720
|
);
|
|
3223
3721
|
}
|
|
3224
|
-
const parsed = (0,
|
|
3722
|
+
const parsed = (0, import_core16.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
|
|
3225
3723
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
3226
3724
|
const source = displayPath2(cwd, file);
|
|
3227
3725
|
const named = scopeFromFilename(file, config);
|
|
@@ -3229,10 +3727,10 @@ function importDotenv(options) {
|
|
|
3229
3727
|
const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
|
|
3230
3728
|
assertEnvironmentAgrees(derived, explicit, source);
|
|
3231
3729
|
const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
|
|
3232
|
-
const environment = (0,
|
|
3730
|
+
const environment = (0, import_core16.lookupEnvironment)(config, explicit ?? derived);
|
|
3233
3731
|
const refs = [];
|
|
3234
3732
|
for (const entry of parsed.entries) {
|
|
3235
|
-
const ref = (0,
|
|
3733
|
+
const ref = (0, import_core16.refFromVariable)(entry.key);
|
|
3236
3734
|
assertImportable(ref, entry.key);
|
|
3237
3735
|
assertNotReserved(ref, entry.key, source, config);
|
|
3238
3736
|
assertRoundTrips(ref, entry.key, config);
|
|
@@ -3360,7 +3858,7 @@ var importCommand = (0, import_citty10.defineCommand)({
|
|
|
3360
3858
|
|
|
3361
3859
|
// src/commands/key.ts
|
|
3362
3860
|
var import_node_crypto = require("crypto");
|
|
3363
|
-
var
|
|
3861
|
+
var import_core17 = require("@penvhq/core");
|
|
3364
3862
|
var import_citty11 = require("citty");
|
|
3365
3863
|
|
|
3366
3864
|
// src/keychain.ts
|
|
@@ -3394,35 +3892,35 @@ function runKeyCreate(options) {
|
|
|
3394
3892
|
const environment = targetEnvironment(project, options.environment);
|
|
3395
3893
|
const declared = project.config.keys?.[environment];
|
|
3396
3894
|
if (declared === void 0) {
|
|
3397
|
-
throw new
|
|
3895
|
+
throw new import_core17.PenvError(
|
|
3398
3896
|
"KEY_SOURCE_UNDECLARED",
|
|
3399
3897
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
3400
3898
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
3401
3899
|
);
|
|
3402
3900
|
}
|
|
3403
|
-
const key = (0, import_node_crypto.randomBytes)(
|
|
3901
|
+
const key = (0, import_node_crypto.randomBytes)(import_core17.KEY_BYTES).toString("base64");
|
|
3404
3902
|
if (declared.source === "keychain") {
|
|
3405
3903
|
const keychain = options.keychain ?? defaultKeychain;
|
|
3406
3904
|
if (options.force !== true) {
|
|
3407
3905
|
let existing;
|
|
3408
3906
|
try {
|
|
3409
|
-
existing = keychain.getPassword(
|
|
3907
|
+
existing = keychain.getPassword(import_core17.KEYCHAIN_SERVICE, declared.id);
|
|
3410
3908
|
} catch (cause) {
|
|
3411
|
-
throw new
|
|
3909
|
+
throw new import_core17.PenvError(
|
|
3412
3910
|
"KEYCHAIN_UNAVAILABLE",
|
|
3413
3911
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
3414
3912
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
3415
3913
|
);
|
|
3416
3914
|
}
|
|
3417
3915
|
if (existing !== null) {
|
|
3418
|
-
throw new
|
|
3916
|
+
throw new import_core17.PenvError(
|
|
3419
3917
|
"KEY_EXISTS",
|
|
3420
3918
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
3421
3919
|
"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."
|
|
3422
3920
|
);
|
|
3423
3921
|
}
|
|
3424
3922
|
}
|
|
3425
|
-
keychain.setPassword(
|
|
3923
|
+
keychain.setPassword(import_core17.KEYCHAIN_SERVICE, declared.id, key);
|
|
3426
3924
|
return { source: "keychain", environment, id: declared.id };
|
|
3427
3925
|
}
|
|
3428
3926
|
return {
|
|
@@ -3436,20 +3934,28 @@ function runKeyCreate(options) {
|
|
|
3436
3934
|
function renderKeyCreate(result2) {
|
|
3437
3935
|
if (result2.source === "keychain") {
|
|
3438
3936
|
return [
|
|
3439
|
-
|
|
3937
|
+
`${out.green(CHECK)} A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
|
|
3440
3938
|
"",
|
|
3441
|
-
|
|
3442
|
-
|
|
3939
|
+
out.dim(
|
|
3940
|
+
"penv kept no copy. Anything sealed under it is unreadable without your keychain, and running"
|
|
3941
|
+
),
|
|
3942
|
+
out.dim(
|
|
3943
|
+
"`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
|
|
3944
|
+
)
|
|
3443
3945
|
];
|
|
3444
3946
|
}
|
|
3445
3947
|
return [
|
|
3446
|
-
|
|
3948
|
+
`${out.green(CHECK)} A new key for environment ${result2.environment}. penv did not store it.`,
|
|
3447
3949
|
"",
|
|
3448
|
-
` ${result2.variable}=${result2.key}`,
|
|
3950
|
+
` ${out.cyan(`${result2.variable}=${result2.key}`)}`,
|
|
3449
3951
|
"",
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3952
|
+
out.dim(
|
|
3953
|
+
"Export it where penv runs, and put it wherever this environment's secrets already live \u2014"
|
|
3954
|
+
),
|
|
3955
|
+
out.dim(
|
|
3956
|
+
"a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable"
|
|
3957
|
+
),
|
|
3958
|
+
out.dim("without it, and penv keeps no copy to fall back on.")
|
|
3453
3959
|
];
|
|
3454
3960
|
}
|
|
3455
3961
|
var keyCommand = (0, import_citty11.defineCommand)({
|
|
@@ -3482,7 +3988,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
|
|
|
3482
3988
|
});
|
|
3483
3989
|
|
|
3484
3990
|
// src/commands/list.ts
|
|
3485
|
-
var
|
|
3991
|
+
var import_core18 = require("@penvhq/core");
|
|
3486
3992
|
var import_citty12 = require("citty");
|
|
3487
3993
|
function scopeLabel2(scope) {
|
|
3488
3994
|
switch (scope.kind) {
|
|
@@ -3495,7 +4001,7 @@ function scopeLabel2(scope) {
|
|
|
3495
4001
|
case "unscoped":
|
|
3496
4002
|
return "default";
|
|
3497
4003
|
default:
|
|
3498
|
-
return (0,
|
|
4004
|
+
return (0, import_core18.assertNever)(scope, "scope");
|
|
3499
4005
|
}
|
|
3500
4006
|
}
|
|
3501
4007
|
async function runList(options) {
|
|
@@ -3503,12 +4009,12 @@ async function runList(options) {
|
|
|
3503
4009
|
const environment = targetEnvironment(project, options.environment);
|
|
3504
4010
|
const keys = keySourceFor(project, environment);
|
|
3505
4011
|
const parameters = [];
|
|
3506
|
-
for (const resolution of await (0,
|
|
4012
|
+
for (const resolution of await (0, import_core18.resolveAll)(environment, project.provider, keys)) {
|
|
3507
4013
|
const winner = resolution.winner;
|
|
3508
4014
|
const scope = winner?.file.scope;
|
|
3509
4015
|
parameters.push({
|
|
3510
4016
|
parameter: resolution.parameter,
|
|
3511
|
-
variable: (0,
|
|
4017
|
+
variable: (0, import_core18.variableName)(resolution.ref, project.config),
|
|
3512
4018
|
scope: scope === void 0 ? "absent" : scopeLabel2(scope),
|
|
3513
4019
|
location: winner?.location,
|
|
3514
4020
|
encrypted: winner?.file.encrypted === true,
|
|
@@ -3517,11 +4023,31 @@ async function runList(options) {
|
|
|
3517
4023
|
}
|
|
3518
4024
|
return { environment, parameters };
|
|
3519
4025
|
}
|
|
4026
|
+
function paintScope(entry) {
|
|
4027
|
+
if (entry.scope === "absent" || entry.viaUnscopedFallback) {
|
|
4028
|
+
return out.yellow(entry.scope);
|
|
4029
|
+
}
|
|
4030
|
+
return out.green(entry.scope);
|
|
4031
|
+
}
|
|
3520
4032
|
function renderList(result2) {
|
|
3521
4033
|
if (result2.parameters.length === 0) {
|
|
3522
|
-
return [
|
|
4034
|
+
return [
|
|
4035
|
+
`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`,
|
|
4036
|
+
tip(`penv set <key> --env ${result2.environment}`)
|
|
4037
|
+
];
|
|
3523
4038
|
}
|
|
3524
|
-
|
|
4039
|
+
const header = ["parameter", "scope", "variable", ""].map((cell) => out.dim(cell));
|
|
4040
|
+
const rows = result2.parameters.map((entry) => [
|
|
4041
|
+
entry.parameter,
|
|
4042
|
+
paintScope(entry),
|
|
4043
|
+
entry.variable,
|
|
4044
|
+
entry.encrypted ? out.dim("encrypted") : ""
|
|
4045
|
+
]);
|
|
4046
|
+
return [
|
|
4047
|
+
heading("penv list", `environment ${result2.environment}`),
|
|
4048
|
+
"",
|
|
4049
|
+
...columns([header, ...rows])
|
|
4050
|
+
];
|
|
3525
4051
|
}
|
|
3526
4052
|
var listCommand = (0, import_citty12.defineCommand)({
|
|
3527
4053
|
meta: { name: "list", description: "List parameters" },
|
|
@@ -3541,7 +4067,7 @@ var listCommand = (0, import_citty12.defineCommand)({
|
|
|
3541
4067
|
});
|
|
3542
4068
|
|
|
3543
4069
|
// src/commands/mv.ts
|
|
3544
|
-
var
|
|
4070
|
+
var import_core19 = require("@penvhq/core");
|
|
3545
4071
|
var import_citty13 = require("citty");
|
|
3546
4072
|
function environmentOf2(file) {
|
|
3547
4073
|
const scope = file.scope;
|
|
@@ -3557,39 +4083,39 @@ async function planFile(project, source, target, parameter) {
|
|
|
3557
4083
|
}
|
|
3558
4084
|
const environment = environmentOf2(source);
|
|
3559
4085
|
if (environment === void 0) {
|
|
3560
|
-
throw new
|
|
4086
|
+
throw new import_core19.PenvError(
|
|
3561
4087
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3562
|
-
`${PENV_DIR}/${(0,
|
|
4088
|
+
`${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
|
|
3563
4089
|
"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."
|
|
3564
4090
|
);
|
|
3565
4091
|
}
|
|
3566
4092
|
const keys = keySourceFor(project, environment);
|
|
3567
|
-
const opened = (0,
|
|
4093
|
+
const opened = (0, import_core19.openValue)(source, stored, keys);
|
|
3568
4094
|
if (opened.kind === "failed") {
|
|
3569
|
-
throw new
|
|
4095
|
+
throw new import_core19.PenvError(
|
|
3570
4096
|
"VALUE_UNDECRYPTABLE",
|
|
3571
|
-
`${PENV_DIR}/${(0,
|
|
4097
|
+
`${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3572
4098
|
"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."
|
|
3573
4099
|
);
|
|
3574
4100
|
}
|
|
3575
4101
|
return {
|
|
3576
4102
|
source,
|
|
3577
4103
|
target,
|
|
3578
|
-
contents: (0,
|
|
4104
|
+
contents: (0, import_core19.sealValue)(target, opened.value, keys, parameter, environment),
|
|
3579
4105
|
resealed: true
|
|
3580
4106
|
};
|
|
3581
4107
|
}
|
|
3582
4108
|
function filesOf(all, ref) {
|
|
3583
|
-
const id = (0,
|
|
3584
|
-
return all.filter((file) => (0,
|
|
4109
|
+
const id = (0, import_core19.parameterId)(ref);
|
|
4110
|
+
return all.filter((file) => (0, import_core19.parameterId)(file) === id);
|
|
3585
4111
|
}
|
|
3586
4112
|
async function runMove(options) {
|
|
3587
4113
|
const project = openProject(options.cwd);
|
|
3588
4114
|
const from = refFromKey(options.from, project.config);
|
|
3589
4115
|
assertWritableKey(options.to);
|
|
3590
4116
|
const to = refFromKey(options.to, project.config);
|
|
3591
|
-
if ((0,
|
|
3592
|
-
throw new
|
|
4117
|
+
if ((0, import_core19.parameterId)(from) === (0, import_core19.parameterId)(to)) {
|
|
4118
|
+
throw new import_core19.PenvError(
|
|
3593
4119
|
"PARAMETER_UNCHANGED",
|
|
3594
4120
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3595
4121
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3599,24 +4125,24 @@ async function runMove(options) {
|
|
|
3599
4125
|
const sources = filesOf(all, from);
|
|
3600
4126
|
const meta = await project.provider.readMeta(from);
|
|
3601
4127
|
if (sources.length === 0 && meta === void 0) {
|
|
3602
|
-
throw new
|
|
4128
|
+
throw new import_core19.PenvError(
|
|
3603
4129
|
"PARAMETER_ABSENT",
|
|
3604
|
-
`Parameter ${(0,
|
|
4130
|
+
`Parameter ${(0, import_core19.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
|
|
3605
4131
|
`\`penv list\` shows every parameter penv holds.`
|
|
3606
4132
|
);
|
|
3607
4133
|
}
|
|
3608
4134
|
const occupied2 = filesOf(all, to);
|
|
3609
4135
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3610
|
-
throw new
|
|
4136
|
+
throw new import_core19.PenvError(
|
|
3611
4137
|
"PARAMETER_EXISTS",
|
|
3612
|
-
`Parameter ${(0,
|
|
3613
|
-
`Remove or rename ${(0,
|
|
4138
|
+
`Parameter ${(0, import_core19.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
|
|
4139
|
+
`Remove or rename ${(0, import_core19.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
3614
4140
|
);
|
|
3615
4141
|
}
|
|
3616
4142
|
const planned = [];
|
|
3617
4143
|
for (const source of sources) {
|
|
3618
4144
|
const target = { ...source, namespace: to.namespace, name: to.name };
|
|
3619
|
-
const one = await planFile(project, source, target, (0,
|
|
4145
|
+
const one = await planFile(project, source, target, (0, import_core19.parameterId)(to));
|
|
3620
4146
|
if (one !== void 0) {
|
|
3621
4147
|
planned.push(one);
|
|
3622
4148
|
}
|
|
@@ -3634,18 +4160,18 @@ async function runMove(options) {
|
|
|
3634
4160
|
await project.provider.removeMeta(from);
|
|
3635
4161
|
}
|
|
3636
4162
|
return {
|
|
3637
|
-
from: (0,
|
|
3638
|
-
to: (0,
|
|
4163
|
+
from: (0, import_core19.parameterId)(from),
|
|
4164
|
+
to: (0, import_core19.parameterId)(to),
|
|
3639
4165
|
files: planned.map((file) => ({
|
|
3640
|
-
from: (0,
|
|
3641
|
-
to: (0,
|
|
4166
|
+
from: (0, import_core19.formatValueFile)(file.source),
|
|
4167
|
+
to: (0, import_core19.formatValueFile)(file.target),
|
|
3642
4168
|
resealed: file.resealed
|
|
3643
4169
|
})),
|
|
3644
4170
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
3645
4171
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
3646
4172
|
// that printed the first while moving the second names no file on disk.
|
|
3647
|
-
meta: meta === void 0 ? void 0 : (0,
|
|
3648
|
-
schema: { was: (0,
|
|
4173
|
+
meta: meta === void 0 ? void 0 : (0, import_core19.formatMetaFile)({ ...to, format: "json" }),
|
|
4174
|
+
schema: { was: (0, import_core19.accessPath)(from).join("."), now: (0, import_core19.accessPath)(to).join(".") }
|
|
3649
4175
|
};
|
|
3650
4176
|
}
|
|
3651
4177
|
function renderMove(result2) {
|
|
@@ -3661,8 +4187,9 @@ function renderMove(result2) {
|
|
|
3661
4187
|
const lines = formatRows(rows);
|
|
3662
4188
|
lines.push(
|
|
3663
4189
|
"",
|
|
3664
|
-
|
|
3665
|
-
|
|
4190
|
+
tip(
|
|
4191
|
+
`.penv/env.ts still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
|
|
4192
|
+
)
|
|
3666
4193
|
);
|
|
3667
4194
|
return lines;
|
|
3668
4195
|
}
|
|
@@ -3688,14 +4215,45 @@ var mvCommand = (0, import_citty13.defineCommand)({
|
|
|
3688
4215
|
});
|
|
3689
4216
|
|
|
3690
4217
|
// src/commands/pull.ts
|
|
4218
|
+
var import_core20 = require("@penvhq/core");
|
|
3691
4219
|
var import_citty14 = require("citty");
|
|
4220
|
+
async function pullProjection(project, source, environment) {
|
|
4221
|
+
const tree = localTree(project);
|
|
4222
|
+
const names = /* @__PURE__ */ new Set();
|
|
4223
|
+
for (const secret of await source.list({ kind: "repository" })) {
|
|
4224
|
+
names.add(secret.name);
|
|
4225
|
+
}
|
|
4226
|
+
for (const secret of await source.list({ kind: "environment", environment })) {
|
|
4227
|
+
names.add(secret.name);
|
|
4228
|
+
}
|
|
4229
|
+
let meta = 0;
|
|
4230
|
+
for (const name of [...names].sort()) {
|
|
4231
|
+
const ref = (0, import_core20.refFromVariable)(name);
|
|
4232
|
+
if (tree.readMetaSync(ref) === void 0) {
|
|
4233
|
+
tree.writeMetaSync(ref, {});
|
|
4234
|
+
meta += 1;
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
return {
|
|
4238
|
+
environment,
|
|
4239
|
+
source: source.type,
|
|
4240
|
+
localSource: false,
|
|
4241
|
+
values: 0,
|
|
4242
|
+
meta,
|
|
4243
|
+
refs: names.size,
|
|
4244
|
+
valuesUnreadable: true
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
3692
4247
|
async function runPull(options) {
|
|
3693
4248
|
const project = openProject(options.cwd);
|
|
3694
|
-
const environment = targetEnvironment(project, options.environment);
|
|
3695
|
-
const source = await sourceProviderFor(project, environment);
|
|
4249
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
4250
|
+
const source = options.source ?? await sourceProviderFor(project, environment);
|
|
3696
4251
|
if (source.type === LOCAL_TREE_TYPE) {
|
|
3697
4252
|
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3698
4253
|
}
|
|
4254
|
+
if ((0, import_core20.holdsProjection)(source)) {
|
|
4255
|
+
return pullProjection(project, source, environment);
|
|
4256
|
+
}
|
|
3699
4257
|
const tree = localTree(project);
|
|
3700
4258
|
const files = await source.list();
|
|
3701
4259
|
let values = 0;
|
|
@@ -3730,6 +4288,22 @@ function renderPull(result2) {
|
|
|
3730
4288
|
}
|
|
3731
4289
|
]);
|
|
3732
4290
|
}
|
|
4291
|
+
if (result2.valuesUnreadable === true) {
|
|
4292
|
+
return formatRows([
|
|
4293
|
+
{
|
|
4294
|
+
glyph: CHECK,
|
|
4295
|
+
label: "Pulled",
|
|
4296
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter name" : "parameter names"}`,
|
|
4297
|
+
detail: `from ${result2.source} for environment ${result2.environment}`
|
|
4298
|
+
},
|
|
4299
|
+
{
|
|
4300
|
+
glyph: WARN,
|
|
4301
|
+
label: "Values not readable",
|
|
4302
|
+
subject: "this destination never returns a secret's value",
|
|
4303
|
+
detail: "fill them locally with `penv set` or `penv fill` \u2014 `penv validate` names every gap"
|
|
4304
|
+
}
|
|
4305
|
+
]);
|
|
4306
|
+
}
|
|
3733
4307
|
return formatRows([
|
|
3734
4308
|
{
|
|
3735
4309
|
glyph: CHECK,
|
|
@@ -3751,13 +4325,17 @@ var pullCommand = (0, import_citty14.defineCommand)({
|
|
|
3751
4325
|
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
3752
4326
|
},
|
|
3753
4327
|
args: {
|
|
3754
|
-
env: {
|
|
4328
|
+
env: {
|
|
4329
|
+
type: "string",
|
|
4330
|
+
description: "The environment to pull (or pass it as a bare flag: --production)"
|
|
4331
|
+
}
|
|
3755
4332
|
},
|
|
3756
4333
|
run({ args }) {
|
|
3757
4334
|
return guard(async () => {
|
|
3758
4335
|
const result2 = await runPull({
|
|
3759
4336
|
cwd: process.cwd(),
|
|
3760
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
4337
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4338
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
3761
4339
|
});
|
|
3762
4340
|
write(renderPull(result2));
|
|
3763
4341
|
});
|
|
@@ -3765,7 +4343,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
|
|
|
3765
4343
|
});
|
|
3766
4344
|
|
|
3767
4345
|
// src/commands/remove.ts
|
|
3768
|
-
var
|
|
4346
|
+
var import_core21 = require("@penvhq/core");
|
|
3769
4347
|
var import_citty15 = require("citty");
|
|
3770
4348
|
async function runRemove(options) {
|
|
3771
4349
|
const project = openProject(options.cwd);
|
|
@@ -3783,12 +4361,12 @@ async function runRemove(options) {
|
|
|
3783
4361
|
continue;
|
|
3784
4362
|
}
|
|
3785
4363
|
await project.provider.remove(file);
|
|
3786
|
-
removed.push((0,
|
|
4364
|
+
removed.push((0, import_core21.formatValueFile)(file));
|
|
3787
4365
|
}
|
|
3788
4366
|
return {
|
|
3789
4367
|
parameter: options.key,
|
|
3790
4368
|
removed,
|
|
3791
|
-
considered: files.map((file) => (0,
|
|
4369
|
+
considered: files.map((file) => (0, import_core21.formatValueFile)(file))
|
|
3792
4370
|
};
|
|
3793
4371
|
}
|
|
3794
4372
|
function renderRemove(result2) {
|
|
@@ -3837,7 +4415,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
|
|
|
3837
4415
|
});
|
|
3838
4416
|
|
|
3839
4417
|
// src/commands/rotate.ts
|
|
3840
|
-
var
|
|
4418
|
+
var import_core22 = require("@penvhq/core");
|
|
3841
4419
|
var import_citty16 = require("citty");
|
|
3842
4420
|
function rotatingFile(ref, environment) {
|
|
3843
4421
|
return {
|
|
@@ -3863,7 +4441,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
3863
4441
|
}
|
|
3864
4442
|
function requireNewValue(value, phase, key) {
|
|
3865
4443
|
if (value === void 0) {
|
|
3866
|
-
throw new
|
|
4444
|
+
throw new import_core22.PenvError(
|
|
3867
4445
|
"ROTATION_NO_VALUE",
|
|
3868
4446
|
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
3869
4447
|
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
@@ -3872,8 +4450,8 @@ function requireNewValue(value, phase, key) {
|
|
|
3872
4450
|
return value;
|
|
3873
4451
|
}
|
|
3874
4452
|
function requireRetaining(provider, environment) {
|
|
3875
|
-
if (!(0,
|
|
3876
|
-
throw new
|
|
4453
|
+
if (!(0, import_core22.retainsPrevious)(provider)) {
|
|
4454
|
+
throw new import_core22.PenvError(
|
|
3877
4455
|
"ROTATION_NOT_RETAINING",
|
|
3878
4456
|
`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`,
|
|
3879
4457
|
"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."
|
|
@@ -3885,12 +4463,20 @@ async function runRotate(options) {
|
|
|
3885
4463
|
const project = openProject(options.cwd);
|
|
3886
4464
|
const environment = targetEnvironment(project, options.environment);
|
|
3887
4465
|
const ref = refFromKey(options.key, project.config);
|
|
3888
|
-
const
|
|
4466
|
+
const source = await sourceProviderFor(project, environment);
|
|
4467
|
+
if (!(0, import_core22.holdsRecords)(source)) {
|
|
4468
|
+
throw new import_core22.PenvError(
|
|
4469
|
+
"ROTATION_NOT_RECORDS",
|
|
4470
|
+
`Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
|
|
4471
|
+
"Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
const provider = source;
|
|
3889
4475
|
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3890
4476
|
const before = await provider.readMeta(ref);
|
|
3891
|
-
const { mechanism } = (0,
|
|
4477
|
+
const { mechanism } = (0, import_core22.rotationOf)(before, environment);
|
|
3892
4478
|
if (mechanism === void 0) {
|
|
3893
|
-
throw new
|
|
4479
|
+
throw new import_core22.PenvError(
|
|
3894
4480
|
"ROTATION_NO_MECHANISM",
|
|
3895
4481
|
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
3896
4482
|
'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.'
|
|
@@ -3900,7 +4486,7 @@ async function runRotate(options) {
|
|
|
3900
4486
|
const complete = options.complete === true;
|
|
3901
4487
|
if (mechanism === "atomic-cutover") {
|
|
3902
4488
|
if (begin || complete) {
|
|
3903
|
-
throw new
|
|
4489
|
+
throw new import_core22.PenvError(
|
|
3904
4490
|
"ROTATION_MECHANISM_MISMATCH",
|
|
3905
4491
|
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
3906
4492
|
"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."
|
|
@@ -3908,13 +4494,13 @@ async function runRotate(options) {
|
|
|
3908
4494
|
}
|
|
3909
4495
|
const value = requireNewValue(options.value, "cutover", options.key);
|
|
3910
4496
|
await writeRotatedValue(project, provider, ref, environment, value);
|
|
3911
|
-
const after2 = (0,
|
|
4497
|
+
const after2 = (0, import_core22.completeRotation)(before, environment, nowIso);
|
|
3912
4498
|
await provider.writeMeta(ref, after2);
|
|
3913
4499
|
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
3914
4500
|
}
|
|
3915
4501
|
const retaining = requireRetaining(provider, environment);
|
|
3916
4502
|
if (begin === complete) {
|
|
3917
|
-
throw new
|
|
4503
|
+
throw new import_core22.PenvError(
|
|
3918
4504
|
"ROTATION_PHASE_REQUIRED",
|
|
3919
4505
|
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
3920
4506
|
"`--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."
|
|
@@ -3923,16 +4509,16 @@ async function runRotate(options) {
|
|
|
3923
4509
|
if (begin) {
|
|
3924
4510
|
const value = requireNewValue(options.value, "begin", options.key);
|
|
3925
4511
|
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
3926
|
-
const after2 = (0,
|
|
4512
|
+
const after2 = (0, import_core22.beginRotation)(before, environment, nowIso);
|
|
3927
4513
|
await retaining.writeMeta(ref, after2);
|
|
3928
4514
|
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
3929
4515
|
}
|
|
3930
|
-
const after = (0,
|
|
4516
|
+
const after = (0, import_core22.completeRotation)(before, environment, nowIso);
|
|
3931
4517
|
await retaining.writeMeta(ref, after);
|
|
3932
4518
|
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
3933
4519
|
}
|
|
3934
4520
|
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
3935
|
-
const { state, rotatingSince, lastRotated } = (0,
|
|
4521
|
+
const { state, rotatingSince, lastRotated } = (0, import_core22.rotationOf)(after, environment);
|
|
3936
4522
|
return {
|
|
3937
4523
|
parameter: [...ref.namespace, ref.name].join("/"),
|
|
3938
4524
|
environment,
|
|
@@ -4019,7 +4605,7 @@ var rotateCommand = (0, import_citty16.defineCommand)({
|
|
|
4019
4605
|
// src/commands/watch.ts
|
|
4020
4606
|
var import_node_fs5 = require("fs");
|
|
4021
4607
|
var import_node_path8 = require("path");
|
|
4022
|
-
var
|
|
4608
|
+
var import_core23 = require("@penvhq/core");
|
|
4023
4609
|
var import_citty17 = require("citty");
|
|
4024
4610
|
var DEBOUNCE_MS = 100;
|
|
4025
4611
|
function runWatch(options) {
|
|
@@ -4152,8 +4738,8 @@ function runWatch(options) {
|
|
|
4152
4738
|
}
|
|
4153
4739
|
addWatcher(project.penvDir, true);
|
|
4154
4740
|
addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
|
|
4155
|
-
if ((0,
|
|
4156
|
-
const schemaFile = (0, import_node_path8.resolve)(project.root, (0,
|
|
4741
|
+
if ((0, import_core23.schemaInsideTree)(project.config) === void 0) {
|
|
4742
|
+
const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core23.schemaFileOf)(project.config));
|
|
4157
4743
|
addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
|
|
4158
4744
|
}
|
|
4159
4745
|
void validate();
|
|
@@ -4191,9 +4777,9 @@ function renderDrift(drift, environment) {
|
|
|
4191
4777
|
if (rows.length === 0) {
|
|
4192
4778
|
return [];
|
|
4193
4779
|
}
|
|
4194
|
-
const lines = ["", `Schema and tree differ for ${environment}
|
|
4780
|
+
const lines = ["", out.bold(`Schema and tree differ for ${environment}:`), ...formatRows(rows)];
|
|
4195
4781
|
for (const remedy of new Set(drift.declared.map((item) => item.remedy))) {
|
|
4196
|
-
lines.push(
|
|
4782
|
+
lines.push(tip(remedy));
|
|
4197
4783
|
}
|
|
4198
4784
|
return lines;
|
|
4199
4785
|
}
|
|
@@ -4224,7 +4810,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
|
|
|
4224
4810
|
process.exitCode = previous;
|
|
4225
4811
|
}
|
|
4226
4812
|
});
|
|
4227
|
-
write([
|
|
4813
|
+
write([`Watching .penv/ and penv.config.ts. ${out.dim("Ctrl-C to stop.")}`]);
|
|
4228
4814
|
await new Promise((resolve7) => {
|
|
4229
4815
|
process.once("SIGINT", () => {
|
|
4230
4816
|
handle.close();
|
|
@@ -4263,7 +4849,7 @@ var main = (0, import_citty18.defineCommand)({
|
|
|
4263
4849
|
}
|
|
4264
4850
|
});
|
|
4265
4851
|
function runMain() {
|
|
4266
|
-
(0,
|
|
4852
|
+
(0, import_core24.setKeychain)(defaultKeychain);
|
|
4267
4853
|
return (0, import_citty18.runMain)(main);
|
|
4268
4854
|
}
|
|
4269
4855
|
// Annotate the CommonJS export names for ESM import in node:
|