@penvhq/cli 0.4.0 → 0.6.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 +929 -444
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -30
- package/dist/index.d.ts +62 -30
- package/dist/index.js +782 -288
- 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,33 +354,33 @@ 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
|
|
329
|
-
function defOf(
|
|
330
|
-
if (typeof
|
|
370
|
+
var import_core4 = require("@penvhq/core");
|
|
371
|
+
function defOf(node2) {
|
|
372
|
+
if (typeof node2 !== "object" || node2 === null) {
|
|
331
373
|
return void 0;
|
|
332
374
|
}
|
|
333
|
-
const def =
|
|
375
|
+
const def = node2.def;
|
|
334
376
|
return typeof def === "object" && def !== null ? def : void 0;
|
|
335
377
|
}
|
|
336
|
-
function typeOf(
|
|
337
|
-
const type = defOf(
|
|
378
|
+
function typeOf(node2) {
|
|
379
|
+
const type = defOf(node2)?.type;
|
|
338
380
|
return typeof type === "string" ? type : void 0;
|
|
339
381
|
}
|
|
340
|
-
function unwrap(
|
|
341
|
-
let current =
|
|
382
|
+
function unwrap(node2) {
|
|
383
|
+
let current = node2;
|
|
342
384
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
343
385
|
const inner = defOf(current)?.innerType;
|
|
344
386
|
if (inner === void 0) {
|
|
@@ -348,38 +390,38 @@ function unwrap(node) {
|
|
|
348
390
|
}
|
|
349
391
|
return current;
|
|
350
392
|
}
|
|
351
|
-
function shapeOf(
|
|
352
|
-
if (typeOf(
|
|
393
|
+
function shapeOf(node2) {
|
|
394
|
+
if (typeOf(node2) !== "object") {
|
|
353
395
|
return void 0;
|
|
354
396
|
}
|
|
355
|
-
const shape =
|
|
397
|
+
const shape = node2.shape;
|
|
356
398
|
return typeof shape === "object" && shape !== null ? shape : void 0;
|
|
357
399
|
}
|
|
358
400
|
function lookup(root, path) {
|
|
359
|
-
let
|
|
401
|
+
let node2 = unwrap(root);
|
|
360
402
|
for (const key of path) {
|
|
361
|
-
const shape = shapeOf(
|
|
403
|
+
const shape = shapeOf(node2);
|
|
362
404
|
if (shape === void 0) {
|
|
363
405
|
return { kind: "unknown" };
|
|
364
406
|
}
|
|
365
407
|
if (!Object.hasOwn(shape, key)) {
|
|
366
408
|
return { kind: "absent" };
|
|
367
409
|
}
|
|
368
|
-
|
|
410
|
+
node2 = unwrap(shape[key]);
|
|
369
411
|
}
|
|
370
|
-
return { kind: "found", node };
|
|
412
|
+
return { kind: "found", node: node2 };
|
|
371
413
|
}
|
|
372
|
-
function minLengthOf(
|
|
373
|
-
if (typeOf(
|
|
414
|
+
function minLengthOf(node2) {
|
|
415
|
+
if (typeOf(node2) !== "string") {
|
|
374
416
|
return void 0;
|
|
375
417
|
}
|
|
376
|
-
const min =
|
|
418
|
+
const min = node2.minLength;
|
|
377
419
|
return typeof min === "number" ? min : void 0;
|
|
378
420
|
}
|
|
379
421
|
var ABSENCE_PERMITTED = /* @__PURE__ */ new Set(["optional", "default", "catch", "prefault"]);
|
|
380
422
|
var ABSENCE_REFUSED = /* @__PURE__ */ new Set(["nullable", "nonoptional", "readonly"]);
|
|
381
|
-
function permitsAbsence(
|
|
382
|
-
let current =
|
|
423
|
+
function permitsAbsence(node2) {
|
|
424
|
+
let current = node2;
|
|
383
425
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
384
426
|
const type = typeOf(current);
|
|
385
427
|
if (type !== void 0 && ABSENCE_PERMITTED.has(type)) {
|
|
@@ -396,13 +438,13 @@ function permitsAbsence(node) {
|
|
|
396
438
|
}
|
|
397
439
|
return void 0;
|
|
398
440
|
}
|
|
399
|
-
function leaves(
|
|
400
|
-
const own = permitsAbsence(
|
|
441
|
+
function leaves(node2, path, inherited, out2) {
|
|
442
|
+
const own = permitsAbsence(node2);
|
|
401
443
|
const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
|
|
402
|
-
const shape = shapeOf(unwrap(
|
|
444
|
+
const shape = shapeOf(unwrap(node2));
|
|
403
445
|
if (shape === void 0) {
|
|
404
446
|
if (path.length > 0) {
|
|
405
|
-
out2.push({ path, absencePermitted, node });
|
|
447
|
+
out2.push({ path, absencePermitted, node: node2 });
|
|
406
448
|
}
|
|
407
449
|
return;
|
|
408
450
|
}
|
|
@@ -418,8 +460,8 @@ function declaredLeaves(schema) {
|
|
|
418
460
|
leaves(schema, [], false, out2);
|
|
419
461
|
return out2;
|
|
420
462
|
}
|
|
421
|
-
function defaultValueOf(
|
|
422
|
-
let current =
|
|
463
|
+
function defaultValueOf(node2) {
|
|
464
|
+
let current = node2;
|
|
423
465
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
424
466
|
const def = defOf(current);
|
|
425
467
|
if (def === void 0) {
|
|
@@ -464,9 +506,9 @@ function computeDrift(input) {
|
|
|
464
506
|
continue;
|
|
465
507
|
}
|
|
466
508
|
const path = leaf.path.join(".");
|
|
467
|
-
const ref = (0,
|
|
509
|
+
const ref = (0, import_core4.refFromAccessPath)(leaf.path);
|
|
468
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.`;
|
|
469
|
-
if (ref === void 0 || (0,
|
|
511
|
+
if (ref === void 0 || (0, import_core4.isReservedToken)(ref.name, config)) {
|
|
470
512
|
if (leaf.absencePermitted) {
|
|
471
513
|
optional.push({ subject: path, remedy: renameRemedy });
|
|
472
514
|
} else {
|
|
@@ -478,13 +520,13 @@ function computeDrift(input) {
|
|
|
478
520
|
}
|
|
479
521
|
continue;
|
|
480
522
|
}
|
|
481
|
-
if (valued.has((0,
|
|
523
|
+
if (valued.has((0, import_core4.parameterId)(ref))) {
|
|
482
524
|
continue;
|
|
483
525
|
}
|
|
484
526
|
if (leaf.absencePermitted) {
|
|
485
527
|
const defaultValue = defaultValueOf(leaf.node);
|
|
486
528
|
optional.push({
|
|
487
|
-
subject: (0,
|
|
529
|
+
subject: (0, import_core4.parameterId)(ref),
|
|
488
530
|
ref,
|
|
489
531
|
...defaultValue === void 0 ? {} : { defaultValue },
|
|
490
532
|
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
|
|
@@ -492,7 +534,7 @@ function computeDrift(input) {
|
|
|
492
534
|
continue;
|
|
493
535
|
}
|
|
494
536
|
declared.push({
|
|
495
|
-
subject: (0,
|
|
537
|
+
subject: (0, import_core4.parameterId)(ref),
|
|
496
538
|
ref,
|
|
497
539
|
remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`,
|
|
498
540
|
detail: `declared in .penv/env.ts, no value for ${environment}`
|
|
@@ -500,12 +542,12 @@ function computeDrift(input) {
|
|
|
500
542
|
}
|
|
501
543
|
const undeclared = [];
|
|
502
544
|
for (const resolution of resolutions) {
|
|
503
|
-
if (lookup(schema, (0,
|
|
545
|
+
if (lookup(schema, (0, import_core4.accessPath)(resolution.ref)).kind !== "absent") {
|
|
504
546
|
continue;
|
|
505
547
|
}
|
|
506
548
|
undeclared.push({
|
|
507
549
|
ref: resolution.ref,
|
|
508
|
-
variable: (0,
|
|
550
|
+
variable: (0, import_core4.variableName)(resolution.ref, config)
|
|
509
551
|
});
|
|
510
552
|
}
|
|
511
553
|
return { declared, undeclared, optional };
|
|
@@ -560,7 +602,7 @@ function padVisible(text, width) {
|
|
|
560
602
|
}
|
|
561
603
|
|
|
562
604
|
// src/ui.ts
|
|
563
|
-
var
|
|
605
|
+
var import_core5 = require("@penvhq/core");
|
|
564
606
|
var CHECK = "\u2713";
|
|
565
607
|
var WARN = "\u26A0";
|
|
566
608
|
var CROSS = "\u2717";
|
|
@@ -634,7 +676,7 @@ function write(lines) {
|
|
|
634
676
|
}
|
|
635
677
|
}
|
|
636
678
|
function reportError(error) {
|
|
637
|
-
if (error instanceof
|
|
679
|
+
if (error instanceof import_core5.PenvError) {
|
|
638
680
|
const suffix = error.remedy === void 0 ? void 0 : `
|
|
639
681
|
${error.remedy}`;
|
|
640
682
|
const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
|
|
@@ -668,31 +710,86 @@ async function guard(run) {
|
|
|
668
710
|
}
|
|
669
711
|
|
|
670
712
|
// src/commands/push.ts
|
|
671
|
-
var
|
|
672
|
-
var import_sink_github = require("@penvhq/sink-github");
|
|
713
|
+
var import_core6 = require("@penvhq/core");
|
|
673
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
|
|
674
759
|
var LAST_PUSHED_KEY = "lastPushedAt";
|
|
675
|
-
function
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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>\`.`
|
|
682
781
|
);
|
|
683
782
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return { sink: override, repo };
|
|
687
|
-
}
|
|
688
|
-
if (declared.type === "github") {
|
|
689
|
-
return { sink: (0, import_sink_github.createGithubSink)(repo === void 0 ? {} : { repo }), repo };
|
|
783
|
+
if (options.provider !== void 0) {
|
|
784
|
+
return { provider: options.provider, location };
|
|
690
785
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 };
|
|
696
793
|
}
|
|
697
794
|
function plan(resolutions, config, environment, allowDecrypt) {
|
|
698
795
|
const outbound = [];
|
|
@@ -704,13 +801,13 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
704
801
|
let encrypted = false;
|
|
705
802
|
if (winner.file.encrypted) {
|
|
706
803
|
if (!allowDecrypt) {
|
|
707
|
-
throw new
|
|
804
|
+
throw new import_core6.PenvError(
|
|
708
805
|
"ENCRYPTED_VALUE_REFUSED",
|
|
709
|
-
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for
|
|
710
|
-
"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."
|
|
711
808
|
);
|
|
712
809
|
}
|
|
713
|
-
(0,
|
|
810
|
+
(0, import_core6.requireValue)(resolution, environment);
|
|
714
811
|
encrypted = true;
|
|
715
812
|
}
|
|
716
813
|
if (resolution.value === void 0) {
|
|
@@ -719,7 +816,7 @@ function plan(resolutions, config, environment, allowDecrypt) {
|
|
|
719
816
|
const scope = winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment };
|
|
720
817
|
outbound.push({
|
|
721
818
|
ref: resolution.ref,
|
|
722
|
-
variable: (0,
|
|
819
|
+
variable: (0, import_core6.variableName)(resolution.ref, config),
|
|
723
820
|
value: resolution.value,
|
|
724
821
|
scope,
|
|
725
822
|
encrypted
|
|
@@ -737,28 +834,65 @@ function recordPush(tree, ref, environment, iso) {
|
|
|
737
834
|
const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
|
|
738
835
|
tree.writeMetaSync(ref, meta);
|
|
739
836
|
}
|
|
740
|
-
async function
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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) {
|
|
744
877
|
const tree = localTree(project);
|
|
745
878
|
const keys = keySourceFor(project, environment);
|
|
746
879
|
const resolutions = resolveAllSync(tree, environment, keys, true);
|
|
747
880
|
const refs = refsFrom(resolutions.map((resolution) => resolution.ref));
|
|
748
|
-
const collision = (0,
|
|
881
|
+
const collision = (0, import_core6.checkNameCollisions)(refs, project.config)[0];
|
|
749
882
|
if (collision !== void 0) {
|
|
750
883
|
throw collision;
|
|
751
884
|
}
|
|
752
|
-
const nameError =
|
|
885
|
+
const nameError = provider.checkNames?.(refs, project.config)[0];
|
|
753
886
|
if (nameError !== void 0) {
|
|
754
887
|
throw nameError;
|
|
755
888
|
}
|
|
756
889
|
const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
|
|
757
|
-
await
|
|
890
|
+
await provider.verify();
|
|
891
|
+
const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
|
|
758
892
|
let repositorySecrets = 0;
|
|
759
893
|
let environmentSecrets = 0;
|
|
760
894
|
for (const item of outbound) {
|
|
761
|
-
await
|
|
895
|
+
await provider.push(item.variable, item.value, item.scope);
|
|
762
896
|
if (item.scope.kind === "repository") {
|
|
763
897
|
repositorySecrets += 1;
|
|
764
898
|
} else {
|
|
@@ -768,15 +902,70 @@ async function runPush(options) {
|
|
|
768
902
|
}
|
|
769
903
|
return {
|
|
770
904
|
environment,
|
|
771
|
-
|
|
905
|
+
destination: provider.type,
|
|
906
|
+
mode: "projection",
|
|
907
|
+
location,
|
|
772
908
|
pushed: outbound.length,
|
|
909
|
+
meta: 0,
|
|
773
910
|
repositorySecrets,
|
|
774
911
|
environmentSecrets,
|
|
775
|
-
decrypted: outbound.filter((item) => item.encrypted).length
|
|
912
|
+
decrypted: outbound.filter((item) => item.encrypted).length,
|
|
913
|
+
createdTarget
|
|
914
|
+
};
|
|
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
|
|
776
956
|
};
|
|
777
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
|
+
}
|
|
778
967
|
function renderPush(result2) {
|
|
779
|
-
if (result2.pushed === 0) {
|
|
968
|
+
if (result2.pushed === 0 && result2.meta === 0) {
|
|
780
969
|
return formatRows([
|
|
781
970
|
{
|
|
782
971
|
glyph: CHECK,
|
|
@@ -785,7 +974,23 @@ function renderPush(result2) {
|
|
|
785
974
|
}
|
|
786
975
|
]);
|
|
787
976
|
}
|
|
788
|
-
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
|
+
}
|
|
789
994
|
const rows = [
|
|
790
995
|
{
|
|
791
996
|
glyph: CHECK,
|
|
@@ -800,12 +1005,20 @@ function renderPush(result2) {
|
|
|
800
1005
|
detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
|
|
801
1006
|
}
|
|
802
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
|
+
}
|
|
803
1016
|
if (result2.decrypted > 0) {
|
|
804
1017
|
rows.push({
|
|
805
1018
|
glyph: WARN,
|
|
806
1019
|
label: "Decrypted",
|
|
807
1020
|
subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
|
|
808
|
-
detail: "sent as plaintext for
|
|
1021
|
+
detail: "sent as plaintext for the destination to re-seal under its own key"
|
|
809
1022
|
});
|
|
810
1023
|
}
|
|
811
1024
|
return formatRows(rows);
|
|
@@ -813,13 +1026,28 @@ function renderPush(result2) {
|
|
|
813
1026
|
var pushCommand = (0, import_citty.defineCommand)({
|
|
814
1027
|
meta: {
|
|
815
1028
|
name: "push",
|
|
816
|
-
description: "Push an environment's
|
|
1029
|
+
description: "Push an environment's values to its provider"
|
|
817
1030
|
},
|
|
818
1031
|
args: {
|
|
819
1032
|
env: { type: "string", description: "The environment to push" },
|
|
820
1033
|
"allow-decrypt": {
|
|
821
1034
|
type: "boolean",
|
|
822
|
-
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"
|
|
823
1051
|
}
|
|
824
1052
|
},
|
|
825
1053
|
run({ args }) {
|
|
@@ -827,7 +1055,22 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
827
1055
|
const result2 = await runPush({
|
|
828
1056
|
cwd: process.cwd(),
|
|
829
1057
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
830
|
-
...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
|
+
])
|
|
831
1074
|
});
|
|
832
1075
|
write(renderPush(result2));
|
|
833
1076
|
});
|
|
@@ -837,7 +1080,7 @@ var pushCommand = (0, import_citty.defineCommand)({
|
|
|
837
1080
|
// src/commands/validate.ts
|
|
838
1081
|
var import_node_path3 = require("path");
|
|
839
1082
|
var import_node_url2 = require("url");
|
|
840
|
-
var
|
|
1083
|
+
var import_core7 = require("@penvhq/core");
|
|
841
1084
|
var import_citty2 = require("citty");
|
|
842
1085
|
var SCHEMA_EXPORT = "schema";
|
|
843
1086
|
var LABELS = {
|
|
@@ -855,10 +1098,10 @@ function issueFrom(error, fallbackSubject) {
|
|
|
855
1098
|
message: firstLine(error.message),
|
|
856
1099
|
...error.remedy === void 0 ? {} : { remedy: error.remedy }
|
|
857
1100
|
};
|
|
858
|
-
if (error instanceof
|
|
1101
|
+
if (error instanceof import_core7.ReservedTokenError) {
|
|
859
1102
|
return { kind: "reserved", subject: error.token, ...base };
|
|
860
1103
|
}
|
|
861
|
-
if (error instanceof
|
|
1104
|
+
if (error instanceof import_core7.NameCollisionError) {
|
|
862
1105
|
return { kind: "collision", subject: error.variable, ...base };
|
|
863
1106
|
}
|
|
864
1107
|
return { kind: "config", subject: fallbackSubject, ...base };
|
|
@@ -868,18 +1111,18 @@ function place(root, path, value) {
|
|
|
868
1111
|
if (leaf === void 0) {
|
|
869
1112
|
return;
|
|
870
1113
|
}
|
|
871
|
-
let
|
|
1114
|
+
let node2 = root;
|
|
872
1115
|
for (const key of path.slice(0, -1)) {
|
|
873
|
-
const existing =
|
|
1116
|
+
const existing = node2[key];
|
|
874
1117
|
if (typeof existing === "object" && existing !== null) {
|
|
875
|
-
|
|
1118
|
+
node2 = existing;
|
|
876
1119
|
continue;
|
|
877
1120
|
}
|
|
878
1121
|
const child = {};
|
|
879
|
-
|
|
880
|
-
|
|
1122
|
+
node2[key] = child;
|
|
1123
|
+
node2 = child;
|
|
881
1124
|
}
|
|
882
|
-
|
|
1125
|
+
node2[leaf] = value;
|
|
883
1126
|
}
|
|
884
1127
|
function isZodType(value) {
|
|
885
1128
|
return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
|
|
@@ -912,16 +1155,16 @@ function exclusively(work) {
|
|
|
912
1155
|
return result2;
|
|
913
1156
|
}
|
|
914
1157
|
function loadSchema(project, environment) {
|
|
915
|
-
const schemaPath = (0,
|
|
1158
|
+
const schemaPath = (0, import_core7.schemaFileOf)(project.config);
|
|
916
1159
|
const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
|
|
917
1160
|
return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
|
|
918
1161
|
}
|
|
919
1162
|
async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
920
|
-
const jiti = (0,
|
|
1163
|
+
const jiti = (0, import_core7.jitiFor)(file);
|
|
921
1164
|
const previous = process.env.PENV_ENV;
|
|
922
|
-
const previousHarvest = process.env[
|
|
1165
|
+
const previousHarvest = process.env[import_core7.SCHEMA_HARVEST_ENV];
|
|
923
1166
|
process.env.PENV_ENV = environment;
|
|
924
|
-
process.env[
|
|
1167
|
+
process.env[import_core7.SCHEMA_HARVEST_ENV] = "1";
|
|
925
1168
|
let loaded;
|
|
926
1169
|
try {
|
|
927
1170
|
loaded = await jiti.import(file);
|
|
@@ -948,9 +1191,9 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
948
1191
|
process.env.PENV_ENV = previous;
|
|
949
1192
|
}
|
|
950
1193
|
if (previousHarvest === void 0) {
|
|
951
|
-
delete process.env[
|
|
1194
|
+
delete process.env[import_core7.SCHEMA_HARVEST_ENV];
|
|
952
1195
|
} else {
|
|
953
|
-
process.env[
|
|
1196
|
+
process.env[import_core7.SCHEMA_HARVEST_ENV] = previousHarvest;
|
|
954
1197
|
}
|
|
955
1198
|
}
|
|
956
1199
|
const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
|
|
@@ -970,14 +1213,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
|
|
|
970
1213
|
}
|
|
971
1214
|
async function runValidate(options) {
|
|
972
1215
|
const project = openProject(options.cwd);
|
|
973
|
-
const environment = targetEnvironment(project, options.environment);
|
|
974
|
-
const schemaPath = (0,
|
|
1216
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
1217
|
+
const schemaPath = (0, import_core7.schemaFileOf)(project.config);
|
|
975
1218
|
const issues = [];
|
|
976
1219
|
let files;
|
|
977
1220
|
try {
|
|
978
1221
|
files = await project.provider.list();
|
|
979
1222
|
} catch (error) {
|
|
980
|
-
if (!(error instanceof
|
|
1223
|
+
if (!(error instanceof import_core7.PenvError)) {
|
|
981
1224
|
throw error;
|
|
982
1225
|
}
|
|
983
1226
|
return {
|
|
@@ -989,17 +1232,17 @@ async function runValidate(options) {
|
|
|
989
1232
|
};
|
|
990
1233
|
}
|
|
991
1234
|
const refs = refsFrom(files);
|
|
992
|
-
for (const error of (0,
|
|
1235
|
+
for (const error of (0, import_core7.validateConfig)(project.config)) {
|
|
993
1236
|
issues.push(issueFrom(error, "penv.config.ts"));
|
|
994
1237
|
}
|
|
995
|
-
for (const collision of (0,
|
|
1238
|
+
for (const collision of (0, import_core7.checkNameCollisions)(refs, project.config)) {
|
|
996
1239
|
issues.push(issueFrom(collision, collision.variable));
|
|
997
1240
|
}
|
|
998
1241
|
const { schema, issues: schemaIssues } = await loadSchema(project, environment);
|
|
999
1242
|
issues.push(...schemaIssues);
|
|
1000
1243
|
let drift = EMPTY_DRIFT;
|
|
1001
1244
|
if (schema !== void 0) {
|
|
1002
|
-
const resolutions = await (0,
|
|
1245
|
+
const resolutions = await (0, import_core7.resolveAll)(
|
|
1003
1246
|
environment,
|
|
1004
1247
|
project.provider,
|
|
1005
1248
|
keySourceFor(project, environment)
|
|
@@ -1008,7 +1251,7 @@ async function runValidate(options) {
|
|
|
1008
1251
|
const undecryptable = /* @__PURE__ */ new Set();
|
|
1009
1252
|
for (const resolution of resolutions) {
|
|
1010
1253
|
if (resolution.undecryptable !== void 0) {
|
|
1011
|
-
undecryptable.add((0,
|
|
1254
|
+
undecryptable.add((0, import_core7.accessPath)(resolution.ref).join("."));
|
|
1012
1255
|
issues.push({
|
|
1013
1256
|
kind: "undecryptable",
|
|
1014
1257
|
subject: resolution.parameter,
|
|
@@ -1018,7 +1261,7 @@ async function runValidate(options) {
|
|
|
1018
1261
|
continue;
|
|
1019
1262
|
}
|
|
1020
1263
|
if (resolution.value !== void 0) {
|
|
1021
|
-
place(object, (0,
|
|
1264
|
+
place(object, (0, import_core7.accessPath)(resolution.ref), resolution.value);
|
|
1022
1265
|
}
|
|
1023
1266
|
}
|
|
1024
1267
|
drift = computeDrift({ schema, resolutions, config: project.config, environment });
|
|
@@ -1085,7 +1328,8 @@ var validateCommand = (0, import_citty2.defineCommand)({
|
|
|
1085
1328
|
return guard(async () => {
|
|
1086
1329
|
const result2 = await runValidate({
|
|
1087
1330
|
cwd: process.cwd(),
|
|
1088
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
1331
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
1332
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1089
1333
|
});
|
|
1090
1334
|
write(renderValidate(result2));
|
|
1091
1335
|
if (!result2.ok) {
|
|
@@ -1108,10 +1352,19 @@ function skipped(check, label) {
|
|
|
1108
1352
|
}
|
|
1109
1353
|
async function runDoctor(options) {
|
|
1110
1354
|
const project = openProject(options.cwd);
|
|
1111
|
-
const environment = targetEnvironment(project, options.environment);
|
|
1355
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
1112
1356
|
const findings = [];
|
|
1113
1357
|
const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
1114
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
|
+
}
|
|
1115
1368
|
const { schema, issues } = await loadSchema(project, environment);
|
|
1116
1369
|
if (schema !== void 0) {
|
|
1117
1370
|
findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
|
|
@@ -1126,7 +1379,7 @@ async function runDoctor(options) {
|
|
|
1126
1379
|
});
|
|
1127
1380
|
}
|
|
1128
1381
|
}
|
|
1129
|
-
const resolutions = await (0,
|
|
1382
|
+
const resolutions = await (0, import_core8.resolveAll)(
|
|
1130
1383
|
environment,
|
|
1131
1384
|
project.provider,
|
|
1132
1385
|
keySourceFor(project, environment)
|
|
@@ -1168,7 +1421,7 @@ async function runDoctor(options) {
|
|
|
1168
1421
|
findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
|
|
1169
1422
|
}
|
|
1170
1423
|
findings.push(...await providerDriftFindings(project, environment, options.source));
|
|
1171
|
-
findings.push(...await
|
|
1424
|
+
findings.push(...await projectionFindings(project, environment, options.projection));
|
|
1172
1425
|
findings.push({
|
|
1173
1426
|
check: "provider",
|
|
1174
1427
|
severity: "pass",
|
|
@@ -1182,7 +1435,7 @@ async function runDoctor(options) {
|
|
|
1182
1435
|
};
|
|
1183
1436
|
}
|
|
1184
1437
|
function missingFindings(subjects, environment) {
|
|
1185
|
-
const required = subjects.filter(({ meta }) => (0,
|
|
1438
|
+
const required = subjects.filter(({ meta }) => (0, import_core8.isRequired)(meta, environment));
|
|
1186
1439
|
const findings = required.filter(({ resolution }) => resolution.winner === void 0).map(({ resolution }) => ({
|
|
1187
1440
|
check: "missing",
|
|
1188
1441
|
severity: "failure",
|
|
@@ -1233,7 +1486,7 @@ function weakFindings(subjects, schema) {
|
|
|
1233
1486
|
if (value === void 0) {
|
|
1234
1487
|
continue;
|
|
1235
1488
|
}
|
|
1236
|
-
const field = lookup(schema, (0,
|
|
1489
|
+
const field = lookup(schema, (0, import_core8.accessPath)(resolution.ref));
|
|
1237
1490
|
if (field.kind !== "found") {
|
|
1238
1491
|
continue;
|
|
1239
1492
|
}
|
|
@@ -1306,7 +1559,7 @@ function fallbackFindings(subjects, environment) {
|
|
|
1306
1559
|
];
|
|
1307
1560
|
}
|
|
1308
1561
|
function plaintextSecretFindings(subjects, environment) {
|
|
1309
|
-
const secrets = subjects.filter(({ meta }) => (0,
|
|
1562
|
+
const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
|
|
1310
1563
|
const findings = [];
|
|
1311
1564
|
for (const { resolution } of secrets) {
|
|
1312
1565
|
const winner = resolution.winner;
|
|
@@ -1346,11 +1599,11 @@ function publicSecretFindings(subjects, environment, config) {
|
|
|
1346
1599
|
}
|
|
1347
1600
|
];
|
|
1348
1601
|
}
|
|
1349
|
-
const secrets = subjects.filter(({ meta }) => (0,
|
|
1602
|
+
const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
|
|
1350
1603
|
const findings = [];
|
|
1351
1604
|
for (const { resolution } of secrets) {
|
|
1352
|
-
const variable = (0,
|
|
1353
|
-
if (!(0,
|
|
1605
|
+
const variable = (0, import_core8.variableName)(resolution.ref, config);
|
|
1606
|
+
if (!(0, import_core8.isPublicVariable)(variable, config)) {
|
|
1354
1607
|
continue;
|
|
1355
1608
|
}
|
|
1356
1609
|
const prefix = prefixes.find((candidate) => variable.startsWith(candidate));
|
|
@@ -1404,15 +1657,6 @@ function encryptionFindings(subjects, environment) {
|
|
|
1404
1657
|
];
|
|
1405
1658
|
}
|
|
1406
1659
|
var EDIT_SKEW_MS = 12e4;
|
|
1407
|
-
function buildSink(declared, override) {
|
|
1408
|
-
if (override !== void 0) {
|
|
1409
|
-
return override;
|
|
1410
|
-
}
|
|
1411
|
-
if (declared.type === "github") {
|
|
1412
|
-
return (0, import_sink_github2.createGithubSink)(declared.repo === void 0 ? {} : { repo: declared.repo });
|
|
1413
|
-
}
|
|
1414
|
-
return void 0;
|
|
1415
|
-
}
|
|
1416
1660
|
function errorDetail(error) {
|
|
1417
1661
|
if (error instanceof Error) {
|
|
1418
1662
|
return error.message.split("\n")[0] ?? error.message;
|
|
@@ -1422,32 +1666,43 @@ function errorDetail(error) {
|
|
|
1422
1666
|
function scopeLabel(scope) {
|
|
1423
1667
|
return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
|
|
1424
1668
|
}
|
|
1425
|
-
async function
|
|
1426
|
-
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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;
|
|
1441
1696
|
}
|
|
1442
1697
|
try {
|
|
1443
|
-
await
|
|
1698
|
+
await projection.verify();
|
|
1444
1699
|
} catch (error) {
|
|
1445
1700
|
return [
|
|
1446
1701
|
{
|
|
1447
|
-
check: "
|
|
1702
|
+
check: "projection-unreachable",
|
|
1448
1703
|
severity: "unknown",
|
|
1449
|
-
label: "
|
|
1450
|
-
subject: `could not reach the ${
|
|
1704
|
+
label: "Destination",
|
|
1705
|
+
subject: `could not reach the ${projection.type} destination for ${environment}`,
|
|
1451
1706
|
detail: errorDetail(error)
|
|
1452
1707
|
}
|
|
1453
1708
|
];
|
|
@@ -1455,20 +1710,20 @@ async function sinkFindings(project, environment, override) {
|
|
|
1455
1710
|
let repoSecrets;
|
|
1456
1711
|
let envSecrets;
|
|
1457
1712
|
try {
|
|
1458
|
-
repoSecrets = await
|
|
1459
|
-
envSecrets = await
|
|
1713
|
+
repoSecrets = await projection.list({ kind: "repository" });
|
|
1714
|
+
envSecrets = await projection.list({ kind: "environment", environment });
|
|
1460
1715
|
} catch (error) {
|
|
1461
1716
|
return [
|
|
1462
1717
|
{
|
|
1463
|
-
check: "
|
|
1718
|
+
check: "projection-unreachable",
|
|
1464
1719
|
severity: "unknown",
|
|
1465
|
-
label: "
|
|
1466
|
-
subject: `could not list secrets in the ${
|
|
1720
|
+
label: "Destination",
|
|
1721
|
+
subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
|
|
1467
1722
|
detail: errorDetail(error)
|
|
1468
1723
|
}
|
|
1469
1724
|
];
|
|
1470
1725
|
}
|
|
1471
|
-
const resolutions = await (0,
|
|
1726
|
+
const resolutions = await (0, import_core8.resolveAll)(
|
|
1472
1727
|
environment,
|
|
1473
1728
|
project.provider,
|
|
1474
1729
|
keySourceFor(project, environment),
|
|
@@ -1482,7 +1737,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1482
1737
|
}
|
|
1483
1738
|
expected.push({
|
|
1484
1739
|
ref: resolution.ref,
|
|
1485
|
-
variable: (0,
|
|
1740
|
+
variable: (0, import_core8.variableName)(resolution.ref, project.config),
|
|
1486
1741
|
scope: winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment }
|
|
1487
1742
|
});
|
|
1488
1743
|
}
|
|
@@ -1501,7 +1756,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1501
1756
|
}
|
|
1502
1757
|
if (!destOf(item.scope).has(key)) {
|
|
1503
1758
|
nameDrift.push({
|
|
1504
|
-
check: "
|
|
1759
|
+
check: "projection-name-drift",
|
|
1505
1760
|
severity: "warning",
|
|
1506
1761
|
label: "Declared, not pushed",
|
|
1507
1762
|
subject: item.variable,
|
|
@@ -1513,7 +1768,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1513
1768
|
for (const secret of repoSecrets) {
|
|
1514
1769
|
if (!allVariables.has(upper(secret.name))) {
|
|
1515
1770
|
nameDrift.push({
|
|
1516
|
-
check: "
|
|
1771
|
+
check: "projection-name-drift",
|
|
1517
1772
|
severity: "warning",
|
|
1518
1773
|
label: "In destination, not declared",
|
|
1519
1774
|
subject: secret.name,
|
|
@@ -1524,7 +1779,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1524
1779
|
for (const secret of envSecrets) {
|
|
1525
1780
|
if (!expectedEnv.has(upper(secret.name))) {
|
|
1526
1781
|
nameDrift.push({
|
|
1527
|
-
check: "
|
|
1782
|
+
check: "projection-name-drift",
|
|
1528
1783
|
severity: "warning",
|
|
1529
1784
|
label: "In destination, not declared",
|
|
1530
1785
|
subject: secret.name,
|
|
@@ -1538,7 +1793,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1538
1793
|
if (secret === void 0) {
|
|
1539
1794
|
continue;
|
|
1540
1795
|
}
|
|
1541
|
-
const pushed = (0,
|
|
1796
|
+
const pushed = (0, import_core8.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
|
|
1542
1797
|
if (typeof pushed !== "string") {
|
|
1543
1798
|
continue;
|
|
1544
1799
|
}
|
|
@@ -1548,7 +1803,7 @@ async function sinkFindings(project, environment, override) {
|
|
|
1548
1803
|
continue;
|
|
1549
1804
|
}
|
|
1550
1805
|
manualEdits.push({
|
|
1551
|
-
check: "
|
|
1806
|
+
check: "projection-manual-edit",
|
|
1552
1807
|
severity: "warning",
|
|
1553
1808
|
label: "Edited outside penv",
|
|
1554
1809
|
subject: item.variable,
|
|
@@ -1559,9 +1814,9 @@ async function sinkFindings(project, environment, override) {
|
|
|
1559
1814
|
findings.push(
|
|
1560
1815
|
...nameDrift.length > 0 ? nameDrift : [
|
|
1561
1816
|
{
|
|
1562
|
-
check: "
|
|
1817
|
+
check: "projection-name-drift",
|
|
1563
1818
|
severity: "pass",
|
|
1564
|
-
label: "
|
|
1819
|
+
label: "Destination names",
|
|
1565
1820
|
subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
|
|
1566
1821
|
}
|
|
1567
1822
|
]
|
|
@@ -1569,17 +1824,17 @@ async function sinkFindings(project, environment, override) {
|
|
|
1569
1824
|
findings.push(
|
|
1570
1825
|
...manualEdits.length > 0 ? manualEdits : [
|
|
1571
1826
|
{
|
|
1572
|
-
check: "
|
|
1827
|
+
check: "projection-manual-edit",
|
|
1573
1828
|
severity: "pass",
|
|
1574
|
-
label: "
|
|
1829
|
+
label: "Destination hand-edits",
|
|
1575
1830
|
subject: `no secret has changed outside penv since its last push for ${environment}`
|
|
1576
1831
|
}
|
|
1577
1832
|
]
|
|
1578
1833
|
);
|
|
1579
1834
|
findings.push({
|
|
1580
|
-
check: "
|
|
1835
|
+
check: "projection-value-drift",
|
|
1581
1836
|
severity: "unknown",
|
|
1582
|
-
label: "
|
|
1837
|
+
label: "Destination values",
|
|
1583
1838
|
subject: "cannot be read back from a write-only destination",
|
|
1584
1839
|
detail: "value drift between the tree and the destination is unknowable by design"
|
|
1585
1840
|
});
|
|
@@ -1608,6 +1863,9 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1608
1863
|
return { kind: "read", subjects: local };
|
|
1609
1864
|
}
|
|
1610
1865
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
1866
|
+
if (!(0, import_core8.holdsRecords)(source)) {
|
|
1867
|
+
return { kind: "read", subjects: local };
|
|
1868
|
+
}
|
|
1611
1869
|
try {
|
|
1612
1870
|
const subjects = await Promise.all(
|
|
1613
1871
|
local.map(async ({ resolution }) => ({
|
|
@@ -1632,11 +1890,11 @@ async function rotationSubjects(project, environment, local, override) {
|
|
|
1632
1890
|
function overdueFindings(subjects, environment, now) {
|
|
1633
1891
|
const findings = [];
|
|
1634
1892
|
for (const { resolution, meta } of subjects) {
|
|
1635
|
-
const { policy, lastRotated } = (0,
|
|
1893
|
+
const { policy, lastRotated } = (0, import_core8.rotationOf)(meta, environment);
|
|
1636
1894
|
if (policy === void 0 || lastRotated === null) {
|
|
1637
1895
|
continue;
|
|
1638
1896
|
}
|
|
1639
|
-
const interval = (0,
|
|
1897
|
+
const interval = (0, import_core8.tryParseDuration)(policy);
|
|
1640
1898
|
if (interval === void 0) {
|
|
1641
1899
|
findings.push({
|
|
1642
1900
|
check: "rotation-overdue",
|
|
@@ -1679,10 +1937,10 @@ function overdueFindings(subjects, environment, now) {
|
|
|
1679
1937
|
function stuckFindings(subjects, environment, now, stuckThresholdMs) {
|
|
1680
1938
|
const findings = [];
|
|
1681
1939
|
for (const { resolution, meta } of subjects) {
|
|
1682
|
-
if (!(0,
|
|
1940
|
+
if (!(0, import_core8.isStuck)(meta, environment, now, stuckThresholdMs)) {
|
|
1683
1941
|
continue;
|
|
1684
1942
|
}
|
|
1685
|
-
const { rotatingSince } = (0,
|
|
1943
|
+
const { rotatingSince } = (0, import_core8.rotationOf)(meta, environment);
|
|
1686
1944
|
if (rotatingSince === null) {
|
|
1687
1945
|
continue;
|
|
1688
1946
|
}
|
|
@@ -1722,7 +1980,7 @@ function scopeKey(scope) {
|
|
|
1722
1980
|
case "environment-local":
|
|
1723
1981
|
return `environment-local:${scope.environment}`;
|
|
1724
1982
|
default:
|
|
1725
|
-
return (0,
|
|
1983
|
+
return (0, import_core8.assertNever)(scope, "scope");
|
|
1726
1984
|
}
|
|
1727
1985
|
}
|
|
1728
1986
|
function relevantToEnvironment(file, environment) {
|
|
@@ -1756,6 +2014,9 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1756
2014
|
];
|
|
1757
2015
|
}
|
|
1758
2016
|
const source = override ?? await sourceProviderFor(project, environment);
|
|
2017
|
+
if (!(0, import_core8.holdsRecords)(source)) {
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
1759
2020
|
let local;
|
|
1760
2021
|
let remote;
|
|
1761
2022
|
try {
|
|
@@ -1779,13 +2040,13 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1779
2040
|
const here = local.get(address);
|
|
1780
2041
|
const there = remote.get(address);
|
|
1781
2042
|
if (here !== void 0 && there !== void 0) {
|
|
1782
|
-
const opened = (0,
|
|
2043
|
+
const opened = (0, import_core8.openValue)(here.file, here.stored, keys);
|
|
1783
2044
|
if (opened.kind !== "plaintext") {
|
|
1784
2045
|
findings.push({
|
|
1785
2046
|
check: "provider-value-drift",
|
|
1786
2047
|
severity: "unknown",
|
|
1787
2048
|
label: "Provider value unreadable",
|
|
1788
|
-
subject: (0,
|
|
2049
|
+
subject: (0, import_core8.formatValueFile)(here.file),
|
|
1789
2050
|
detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
|
|
1790
2051
|
});
|
|
1791
2052
|
continue;
|
|
@@ -1795,7 +2056,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1795
2056
|
check: "provider-value-drift",
|
|
1796
2057
|
severity: "failure",
|
|
1797
2058
|
label: "Provider value drift",
|
|
1798
|
-
subject: (0,
|
|
2059
|
+
subject: (0, import_core8.formatValueFile)(here.file),
|
|
1799
2060
|
detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
|
|
1800
2061
|
});
|
|
1801
2062
|
}
|
|
@@ -1809,7 +2070,7 @@ async function providerDriftFindings(project, environment, override) {
|
|
|
1809
2070
|
check: "provider-value-drift",
|
|
1810
2071
|
severity: "warning",
|
|
1811
2072
|
label: here !== void 0 ? "Only in the local tree" : "Only in the source",
|
|
1812
|
-
subject: (0,
|
|
2073
|
+
subject: (0, import_core8.formatValueFile)(present.file),
|
|
1813
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`
|
|
1814
2075
|
});
|
|
1815
2076
|
}
|
|
@@ -1875,7 +2136,7 @@ function renderDoctor(report) {
|
|
|
1875
2136
|
var doctorCommand = (0, import_citty3.defineCommand)({
|
|
1876
2137
|
meta: {
|
|
1877
2138
|
name: "doctor",
|
|
1878
|
-
description: "Report missing, weak, unused, fallback, plaintext-secret, and
|
|
2139
|
+
description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
|
|
1879
2140
|
},
|
|
1880
2141
|
args: {
|
|
1881
2142
|
env: { type: "string", description: "The environment to report on" }
|
|
@@ -1884,7 +2145,8 @@ var doctorCommand = (0, import_citty3.defineCommand)({
|
|
|
1884
2145
|
return guard(async () => {
|
|
1885
2146
|
const report = await runDoctor({
|
|
1886
2147
|
cwd: process.cwd(),
|
|
1887
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
2148
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
2149
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
1888
2150
|
});
|
|
1889
2151
|
write(renderDoctor(report));
|
|
1890
2152
|
if (!report.ok) {
|
|
@@ -1895,11 +2157,11 @@ var doctorCommand = (0, import_citty3.defineCommand)({
|
|
|
1895
2157
|
});
|
|
1896
2158
|
|
|
1897
2159
|
// src/commands/encrypt.ts
|
|
1898
|
-
var
|
|
2160
|
+
var import_core10 = require("@penvhq/core");
|
|
1899
2161
|
var import_citty5 = require("citty");
|
|
1900
2162
|
|
|
1901
2163
|
// src/commands/set.ts
|
|
1902
|
-
var
|
|
2164
|
+
var import_core9 = require("@penvhq/core");
|
|
1903
2165
|
var import_citty4 = require("citty");
|
|
1904
2166
|
function scopeFrom(options) {
|
|
1905
2167
|
if (options.local === true) {
|
|
@@ -1919,7 +2181,7 @@ function targetScope(project, options, key) {
|
|
|
1919
2181
|
return scopeFrom(options);
|
|
1920
2182
|
}
|
|
1921
2183
|
if (environment.trim().length === 0) {
|
|
1922
|
-
throw new
|
|
2184
|
+
throw new import_core9.PenvError(
|
|
1923
2185
|
"ENVIRONMENT_FLAG_EMPTY",
|
|
1924
2186
|
`\`--env\` for parameter ${key} names no environment`,
|
|
1925
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.`
|
|
@@ -1932,27 +2194,27 @@ function policyEnvironment(project, options) {
|
|
|
1932
2194
|
}
|
|
1933
2195
|
function sealFor(project, file, value, parameter, environment) {
|
|
1934
2196
|
if (environment === void 0) {
|
|
1935
|
-
throw new
|
|
2197
|
+
throw new import_core9.PenvError(
|
|
1936
2198
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
1937
|
-
`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`,
|
|
1938
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."
|
|
1939
2201
|
);
|
|
1940
2202
|
}
|
|
1941
|
-
return (0,
|
|
2203
|
+
return (0, import_core9.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
|
|
1942
2204
|
}
|
|
1943
2205
|
async function sealAwareWrite(options) {
|
|
1944
2206
|
const { project, provider, ref, scope, value, environment } = options;
|
|
1945
|
-
const secret = (0,
|
|
2207
|
+
const secret = (0, import_core9.isSecret)(await provider.readMeta(ref), environment);
|
|
1946
2208
|
const file = {
|
|
1947
2209
|
namespace: ref.namespace,
|
|
1948
2210
|
name: ref.name,
|
|
1949
2211
|
scope,
|
|
1950
2212
|
encrypted: secret
|
|
1951
2213
|
};
|
|
1952
|
-
const stored = secret ? sealFor(project, file, value, (0,
|
|
2214
|
+
const stored = secret ? sealFor(project, file, value, (0, import_core9.parameterId)(ref), environment) : value;
|
|
1953
2215
|
await provider.write(file, stored);
|
|
1954
2216
|
await provider.remove({ ...file, encrypted: !secret });
|
|
1955
|
-
return { encrypted: secret, location: (0,
|
|
2217
|
+
return { encrypted: secret, location: (0, import_core9.formatValueFile)(file) };
|
|
1956
2218
|
}
|
|
1957
2219
|
async function runSet(options) {
|
|
1958
2220
|
const project = openProject(options.cwd);
|
|
@@ -2034,7 +2296,7 @@ function twins(project, key, options) {
|
|
|
2034
2296
|
}
|
|
2035
2297
|
function environmentFor(project, options, verb) {
|
|
2036
2298
|
if (options.environment === void 0) {
|
|
2037
|
-
throw new
|
|
2299
|
+
throw new import_core10.PenvError(
|
|
2038
2300
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
2039
2301
|
`\`penv ${verb}\` names no environment, and keys are declared per environment`,
|
|
2040
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."
|
|
@@ -2049,32 +2311,32 @@ async function runEncrypt(options) {
|
|
|
2049
2311
|
const project = openProject(options.cwd);
|
|
2050
2312
|
const environment = environmentFor(project, options, "encrypt");
|
|
2051
2313
|
const [plain, sealed] = twins(project, options.key, options);
|
|
2052
|
-
const parameter = (0,
|
|
2314
|
+
const parameter = (0, import_core10.parameterId)(plain);
|
|
2053
2315
|
const value = await readOne(project, plain);
|
|
2054
2316
|
if (value === void 0) {
|
|
2055
2317
|
const already = await readOne(project, sealed);
|
|
2056
|
-
throw new
|
|
2318
|
+
throw new import_core10.PenvError(
|
|
2057
2319
|
"PARAMETER_ABSENT",
|
|
2058
|
-
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)}`,
|
|
2059
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."
|
|
2060
2322
|
);
|
|
2061
2323
|
}
|
|
2062
|
-
const text = (0,
|
|
2324
|
+
const text = (0, import_core10.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
|
|
2063
2325
|
await project.provider.write(sealed, text);
|
|
2064
2326
|
await project.provider.remove(plain);
|
|
2065
2327
|
return {
|
|
2066
2328
|
parameter,
|
|
2067
|
-
location: (0,
|
|
2068
|
-
removed: (0,
|
|
2329
|
+
location: (0, import_core10.formatValueFile)(sealed),
|
|
2330
|
+
removed: (0, import_core10.formatValueFile)(plain)
|
|
2069
2331
|
};
|
|
2070
2332
|
}
|
|
2071
2333
|
async function runDecrypt(options) {
|
|
2072
2334
|
const project = openProject(options.cwd);
|
|
2073
2335
|
const environment = environmentFor(project, options, "decrypt");
|
|
2074
2336
|
const [plain, sealed] = twins(project, options.key, options);
|
|
2075
|
-
const parameter = (0,
|
|
2076
|
-
if ((0,
|
|
2077
|
-
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(
|
|
2078
2340
|
"SECRET_DECRYPT_REFUSED",
|
|
2079
2341
|
`Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
|
|
2080
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."
|
|
@@ -2082,18 +2344,18 @@ async function runDecrypt(options) {
|
|
|
2082
2344
|
}
|
|
2083
2345
|
const stored = await readOne(project, sealed);
|
|
2084
2346
|
if (stored === void 0) {
|
|
2085
|
-
throw new
|
|
2347
|
+
throw new import_core10.PenvError(
|
|
2086
2348
|
"PARAMETER_ABSENT",
|
|
2087
|
-
`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)}`,
|
|
2088
2350
|
`Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
|
|
2089
2351
|
);
|
|
2090
2352
|
}
|
|
2091
|
-
const opened = (0,
|
|
2353
|
+
const opened = (0, import_core10.openValue)(sealed, stored, keySourceFor(project, environment));
|
|
2092
2354
|
if (opened.kind === "failed") {
|
|
2093
2355
|
throw new UndecryptableAt(
|
|
2094
2356
|
parameter,
|
|
2095
2357
|
environment,
|
|
2096
|
-
(0,
|
|
2358
|
+
(0, import_core10.formatValueFile)(sealed),
|
|
2097
2359
|
opened.failure.detail
|
|
2098
2360
|
);
|
|
2099
2361
|
}
|
|
@@ -2101,11 +2363,11 @@ async function runDecrypt(options) {
|
|
|
2101
2363
|
await project.provider.remove(sealed);
|
|
2102
2364
|
return {
|
|
2103
2365
|
parameter,
|
|
2104
|
-
location: (0,
|
|
2105
|
-
removed: (0,
|
|
2366
|
+
location: (0, import_core10.formatValueFile)(plain),
|
|
2367
|
+
removed: (0, import_core10.formatValueFile)(sealed)
|
|
2106
2368
|
};
|
|
2107
2369
|
}
|
|
2108
|
-
var UndecryptableAt = class extends
|
|
2370
|
+
var UndecryptableAt = class extends import_core10.PenvError {
|
|
2109
2371
|
constructor(parameter, environment, location, detail) {
|
|
2110
2372
|
super(
|
|
2111
2373
|
"VALUE_UNDECRYPTABLE",
|
|
@@ -2160,57 +2422,14 @@ var decryptCommand = (0, import_citty5.defineCommand)({
|
|
|
2160
2422
|
});
|
|
2161
2423
|
|
|
2162
2424
|
// src/commands/fill.ts
|
|
2163
|
-
var
|
|
2425
|
+
var import_core11 = require("@penvhq/core");
|
|
2164
2426
|
var import_citty6 = require("citty");
|
|
2165
|
-
|
|
2166
|
-
// src/input.ts
|
|
2167
|
-
var import_node_readline = require("readline");
|
|
2168
|
-
function lineReader(input = process.stdin, output = process.stdout) {
|
|
2169
|
-
const rl = (0, import_node_readline.createInterface)({ input, output });
|
|
2170
|
-
const buffered = [];
|
|
2171
|
-
const waiting = [];
|
|
2172
|
-
let closed = false;
|
|
2173
|
-
rl.on("line", (line) => {
|
|
2174
|
-
const next = waiting.shift();
|
|
2175
|
-
if (next === void 0) {
|
|
2176
|
-
buffered.push(line);
|
|
2177
|
-
} else {
|
|
2178
|
-
next(line);
|
|
2179
|
-
}
|
|
2180
|
-
});
|
|
2181
|
-
rl.on("close", () => {
|
|
2182
|
-
closed = true;
|
|
2183
|
-
for (const next of waiting.splice(0)) {
|
|
2184
|
-
next(void 0);
|
|
2185
|
-
}
|
|
2186
|
-
});
|
|
2187
|
-
return {
|
|
2188
|
-
ask(question) {
|
|
2189
|
-
const early = buffered.shift();
|
|
2190
|
-
if (early !== void 0) {
|
|
2191
|
-
return Promise.resolve(early);
|
|
2192
|
-
}
|
|
2193
|
-
if (closed) {
|
|
2194
|
-
return Promise.resolve(void 0);
|
|
2195
|
-
}
|
|
2196
|
-
rl.setPrompt(question);
|
|
2197
|
-
rl.prompt();
|
|
2198
|
-
return new Promise((resolve7) => {
|
|
2199
|
-
waiting.push(resolve7);
|
|
2200
|
-
});
|
|
2201
|
-
},
|
|
2202
|
-
close() {
|
|
2203
|
-
rl.close();
|
|
2204
|
-
}
|
|
2205
|
-
};
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
// src/commands/fill.ts
|
|
2209
2427
|
var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
|
|
2210
2428
|
async function runFill(options) {
|
|
2211
2429
|
const validation = await runValidate({
|
|
2212
2430
|
cwd: options.cwd,
|
|
2213
|
-
...options.environment === void 0 ? {} : { environment: options.environment }
|
|
2431
|
+
...options.environment === void 0 ? {} : { environment: options.environment },
|
|
2432
|
+
...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
|
|
2214
2433
|
});
|
|
2215
2434
|
const environment = validation.environment;
|
|
2216
2435
|
const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
|
|
@@ -2218,7 +2437,7 @@ async function runFill(options) {
|
|
|
2218
2437
|
const detail = blockers.map(
|
|
2219
2438
|
(issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
|
|
2220
2439
|
).join("\n");
|
|
2221
|
-
throw new
|
|
2440
|
+
throw new import_core11.PenvError(
|
|
2222
2441
|
"FILL_BLOCKED",
|
|
2223
2442
|
`penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
|
|
2224
2443
|
${detail}`,
|
|
@@ -2340,7 +2559,8 @@ var fillCommand = (0, import_citty6.defineCommand)({
|
|
|
2340
2559
|
await runFill({
|
|
2341
2560
|
cwd: process.cwd(),
|
|
2342
2561
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2343
|
-
ask
|
|
2562
|
+
ask,
|
|
2563
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
2344
2564
|
})
|
|
2345
2565
|
)
|
|
2346
2566
|
);
|
|
@@ -2354,14 +2574,14 @@ var fillCommand = (0, import_citty6.defineCommand)({
|
|
|
2354
2574
|
// src/commands/generate.ts
|
|
2355
2575
|
var import_node_fs = require("fs");
|
|
2356
2576
|
var import_node_path4 = require("path");
|
|
2357
|
-
var
|
|
2577
|
+
var import_core12 = require("@penvhq/core");
|
|
2358
2578
|
var import_citty7 = require("citty");
|
|
2359
2579
|
var DEFAULT_OUTPUT = ".env";
|
|
2360
2580
|
function entriesFor(project, environment, allowDecrypt) {
|
|
2361
2581
|
const keys = keySourceFor(project, environment);
|
|
2362
2582
|
const tree = localTree(project);
|
|
2363
2583
|
const resolutions = resolveAllSync(tree, environment, keys);
|
|
2364
|
-
const collision = (0,
|
|
2584
|
+
const collision = (0, import_core12.checkNameCollisions)(
|
|
2365
2585
|
refsFrom(resolutions.map((resolution) => resolution.ref)),
|
|
2366
2586
|
project.config
|
|
2367
2587
|
)[0];
|
|
@@ -2374,21 +2594,21 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2374
2594
|
const winner = resolution.winner;
|
|
2375
2595
|
if (winner?.file.encrypted === true) {
|
|
2376
2596
|
if (!allowDecrypt) {
|
|
2377
|
-
throw new
|
|
2597
|
+
throw new import_core12.PenvError(
|
|
2378
2598
|
"ENCRYPTED_VALUE_REFUSED",
|
|
2379
2599
|
`Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
|
|
2380
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.`
|
|
2381
2601
|
);
|
|
2382
2602
|
}
|
|
2383
|
-
(0,
|
|
2603
|
+
(0, import_core12.requireValue)(resolution, environment);
|
|
2384
2604
|
decrypted += 1;
|
|
2385
2605
|
}
|
|
2386
2606
|
if (resolution.value === void 0) {
|
|
2387
2607
|
continue;
|
|
2388
2608
|
}
|
|
2389
|
-
const description = (0,
|
|
2609
|
+
const description = (0, import_core12.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
|
|
2390
2610
|
entries.push({
|
|
2391
|
-
key: (0,
|
|
2611
|
+
key: (0, import_core12.variableName)(resolution.ref, project.config),
|
|
2392
2612
|
value: resolution.value,
|
|
2393
2613
|
...typeof description === "string" ? { description } : {}
|
|
2394
2614
|
});
|
|
@@ -2398,15 +2618,15 @@ function entriesFor(project, environment, allowDecrypt) {
|
|
|
2398
2618
|
}
|
|
2399
2619
|
function generateDotenv(options) {
|
|
2400
2620
|
const project = openProject(options.cwd);
|
|
2401
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2402
|
-
return (0,
|
|
2621
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2622
|
+
return (0, import_core12.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
|
|
2403
2623
|
}
|
|
2404
2624
|
function runGenerate(options) {
|
|
2405
2625
|
const project = openProject(options.cwd);
|
|
2406
|
-
const environment = targetEnvironment(project, options.environment);
|
|
2626
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
2407
2627
|
const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
|
|
2408
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);
|
|
2409
|
-
(0, import_node_fs.writeFileSync)(file, (0,
|
|
2629
|
+
(0, import_node_fs.writeFileSync)(file, (0, import_core12.serializeDotenv)(entries), "utf8");
|
|
2410
2630
|
return { file, environment, entries: entries.length, decrypted };
|
|
2411
2631
|
}
|
|
2412
2632
|
function displayPath(cwd, file) {
|
|
@@ -2449,7 +2669,8 @@ var generateCommand = (0, import_citty7.defineCommand)({
|
|
|
2449
2669
|
cwd,
|
|
2450
2670
|
...args.env === void 0 ? {} : { environment: args.env },
|
|
2451
2671
|
...args.out === void 0 ? {} : { out: args.out },
|
|
2452
|
-
...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"])
|
|
2453
2674
|
});
|
|
2454
2675
|
write(renderGenerate(result2, cwd));
|
|
2455
2676
|
});
|
|
@@ -2457,17 +2678,17 @@ var generateCommand = (0, import_citty7.defineCommand)({
|
|
|
2457
2678
|
});
|
|
2458
2679
|
|
|
2459
2680
|
// src/commands/get.ts
|
|
2460
|
-
var
|
|
2681
|
+
var import_core13 = require("@penvhq/core");
|
|
2461
2682
|
var import_citty8 = require("citty");
|
|
2462
2683
|
async function runGet(options) {
|
|
2463
2684
|
const project = openProject(options.cwd);
|
|
2464
2685
|
const environment = targetEnvironment(project, options.environment);
|
|
2465
2686
|
const ref = refFromKey(options.key);
|
|
2466
2687
|
const keys = keySourceFor(project, environment);
|
|
2467
|
-
const resolution = await (0,
|
|
2468
|
-
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);
|
|
2469
2690
|
if (value === void 0) {
|
|
2470
|
-
throw new
|
|
2691
|
+
throw new import_core13.PenvError(
|
|
2471
2692
|
"PARAMETER_ABSENT",
|
|
2472
2693
|
`Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
|
|
2473
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.`
|
|
@@ -2489,7 +2710,7 @@ async function runExplain(options) {
|
|
|
2489
2710
|
const environment = targetEnvironment(project, options.environment);
|
|
2490
2711
|
const ref = refFromKey(options.key);
|
|
2491
2712
|
const keys = keySourceFor(project, environment);
|
|
2492
|
-
const resolution = await (0,
|
|
2713
|
+
const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
|
|
2493
2714
|
const winner = resolution.winner;
|
|
2494
2715
|
return {
|
|
2495
2716
|
parameter: resolution.parameter,
|
|
@@ -2545,15 +2766,17 @@ var getCommand = (0, import_citty8.defineCommand)({
|
|
|
2545
2766
|
// src/commands/import.ts
|
|
2546
2767
|
var import_node_fs4 = require("fs");
|
|
2547
2768
|
var import_node_path7 = require("path");
|
|
2548
|
-
var
|
|
2769
|
+
var import_core16 = require("@penvhq/core");
|
|
2549
2770
|
var import_citty10 = require("citty");
|
|
2550
2771
|
|
|
2551
2772
|
// src/detect.ts
|
|
2552
2773
|
var import_node_fs2 = require("fs");
|
|
2553
2774
|
var import_node_path5 = require("path");
|
|
2554
|
-
var
|
|
2775
|
+
var import_core14 = require("@penvhq/core");
|
|
2555
2776
|
var SIGNATURES = [
|
|
2556
2777
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
2778
|
+
{ name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
|
|
2779
|
+
{ name: "Nuxt", packages: ["nuxt"], publicPrefixes: ["NUXT_PUBLIC_"] },
|
|
2557
2780
|
{
|
|
2558
2781
|
name: "TanStack Start",
|
|
2559
2782
|
packages: ["@tanstack/react-start", "@tanstack/start"],
|
|
@@ -2589,7 +2812,7 @@ function schemaFileFor(cwd) {
|
|
|
2589
2812
|
if (!occupied(cwd, beside)) {
|
|
2590
2813
|
return { file: beside, displaced: preferred };
|
|
2591
2814
|
}
|
|
2592
|
-
return { file:
|
|
2815
|
+
return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2593
2816
|
}
|
|
2594
2817
|
function dependenciesOf(cwd) {
|
|
2595
2818
|
const manifest = manifestOf(cwd);
|
|
@@ -2627,17 +2850,23 @@ function detectFramework(cwd) {
|
|
|
2627
2850
|
}
|
|
2628
2851
|
for (const signature of SIGNATURES) {
|
|
2629
2852
|
if (signature.packages.some((name) => dependencies.has(name))) {
|
|
2630
|
-
|
|
2631
|
-
return {
|
|
2632
|
-
name: signature.name,
|
|
2633
|
-
schemaFile: schema.file,
|
|
2634
|
-
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2635
|
-
publicPrefixes: signature.publicPrefixes
|
|
2636
|
-
};
|
|
2853
|
+
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2637
2854
|
}
|
|
2638
2855
|
}
|
|
2856
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2857
|
+
return detectedFrom(cwd, "Bun", []);
|
|
2858
|
+
}
|
|
2639
2859
|
return void 0;
|
|
2640
2860
|
}
|
|
2861
|
+
function detectedFrom(cwd, name, publicPrefixes) {
|
|
2862
|
+
const schema = schemaFileFor(cwd);
|
|
2863
|
+
return {
|
|
2864
|
+
name,
|
|
2865
|
+
schemaFile: schema.file,
|
|
2866
|
+
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2867
|
+
publicPrefixes
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2641
2870
|
var DEFAULT_ALIAS = "@env";
|
|
2642
2871
|
var IMPORTS_ALIAS = "#env";
|
|
2643
2872
|
function detectAlias(cwd) {
|
|
@@ -2653,8 +2882,134 @@ function hasImportsBlock(cwd) {
|
|
|
2653
2882
|
var import_node_fs3 = require("fs");
|
|
2654
2883
|
var import_node_path6 = require("path");
|
|
2655
2884
|
var import_promises = require("readline/promises");
|
|
2656
|
-
var
|
|
2885
|
+
var import_core15 = require("@penvhq/core");
|
|
2657
2886
|
var import_citty9 = require("citty");
|
|
2887
|
+
|
|
2888
|
+
// src/seams.ts
|
|
2889
|
+
function nextjs({ alias, srcDir }) {
|
|
2890
|
+
return {
|
|
2891
|
+
kind: "scaffold",
|
|
2892
|
+
file: `${srcDir}instrumentation.ts`,
|
|
2893
|
+
content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
|
|
2894
|
+
// before any of your app code, on every server boot. penv put one line here so
|
|
2895
|
+
// your config is in process.env before the first library reads it. The filename
|
|
2896
|
+
// is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
|
|
2897
|
+
export async function register() {
|
|
2898
|
+
// Next also calls register on the Edge runtime, where penv cannot read the
|
|
2899
|
+
// filesystem \u2014 so inject runs on the Node runtime only.
|
|
2900
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
2901
|
+
await import("${alias}");
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
`,
|
|
2905
|
+
ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
|
|
2906
|
+
if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
|
|
2907
|
+
notes: []
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
function sveltekit({ alias }) {
|
|
2911
|
+
return {
|
|
2912
|
+
kind: "scaffold",
|
|
2913
|
+
// SvelteKit's hooks module is always under `src/`, whatever the schema path.
|
|
2914
|
+
file: "src/hooks.server.ts",
|
|
2915
|
+
content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
|
|
2916
|
+
// your routes handle a request. penv put this import first so your config is in
|
|
2917
|
+
// process.env before any library reads it. Keep it the first line; the name and
|
|
2918
|
+
// location are SvelteKit's, not penv's.
|
|
2919
|
+
import "${alias}";
|
|
2920
|
+
`,
|
|
2921
|
+
ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
|
|
2922
|
+
notes: [
|
|
2923
|
+
`SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
|
|
2924
|
+
]
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
function nuxt({ alias }) {
|
|
2928
|
+
return {
|
|
2929
|
+
kind: "scaffold",
|
|
2930
|
+
// The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
|
|
2931
|
+
// so it must sort first to run before any other plugin reads process.env.
|
|
2932
|
+
file: "server/plugins/0.penv.ts",
|
|
2933
|
+
content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
|
|
2934
|
+
// before any middleware or request handler). penv put the import here so your
|
|
2935
|
+
// config is in process.env before a library reads it. The \`0.\` prefix keeps this
|
|
2936
|
+
// plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
|
|
2937
|
+
import "${alias}";
|
|
2938
|
+
|
|
2939
|
+
// Nitro requires every plugin file to default-export a function; the injection
|
|
2940
|
+
// above already ran at import, so this body is intentionally empty.
|
|
2941
|
+
export default defineNitroPlugin(() => {});
|
|
2942
|
+
`,
|
|
2943
|
+
ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
|
|
2944
|
+
notes: [
|
|
2945
|
+
`Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
|
|
2946
|
+
]
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
function bun({ alias }) {
|
|
2950
|
+
return {
|
|
2951
|
+
kind: "scaffold",
|
|
2952
|
+
file: ".penv/preload.ts",
|
|
2953
|
+
content: `// Bun evaluates this file before your entry point, because it is listed under
|
|
2954
|
+
// \`preload\` in bunfig.toml (see the note penv printed). Running it injects your
|
|
2955
|
+
// config into process.env before any library reads it.
|
|
2956
|
+
import "${alias}";
|
|
2957
|
+
`,
|
|
2958
|
+
ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
|
|
2959
|
+
notes: [
|
|
2960
|
+
'Register it in bunfig.toml so Bun runs it: `preload = ["./.penv/preload.ts"]` (and the same under `[test]`).',
|
|
2961
|
+
`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`
|
|
2962
|
+
]
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
function node({ alias, srcDir }) {
|
|
2966
|
+
const runtimeSpecifier = alias.startsWith("#") ? alias : `./${srcDir}.penv/env.js`;
|
|
2967
|
+
return {
|
|
2968
|
+
kind: "instruct",
|
|
2969
|
+
instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
|
|
2970
|
+
code, so place the injection yourself, one of two ways (both run first):
|
|
2971
|
+
\u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
|
|
2972
|
+
\u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
|
|
2973
|
+
` + (alias.startsWith("#") ? "" : `Note: \`${alias}\` is a tsconfig alias that Node does not resolve at runtime \u2014 the paths above point at the built schema module instead.
|
|
2974
|
+
`)
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function tanstack({ alias, srcDir }) {
|
|
2978
|
+
return {
|
|
2979
|
+
kind: "instruct",
|
|
2980
|
+
instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
|
|
2981
|
+
server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
|
|
2982
|
+
import before it. If you have no server.ts yet, create it from TanStack's server-entry
|
|
2983
|
+
template and put that import first.`
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
function astro(_context) {
|
|
2987
|
+
return {
|
|
2988
|
+
kind: "instruct",
|
|
2989
|
+
instruction: 'Astro has no single pre-app hook. Add a small integration to astro.config that\ninjects the import into every SSR page, then cover the routes it cannot reach:\n \u2022 In astro.config integrations, add:\n { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>\n injectScript("page-ssr", \'import "@env";\') } }\n \u2022 Also add `import "@env";` as the first line of src/middleware.ts and of any\n endpoint route (src/pages/*.ts) that imports a library reading process.env.\nInjection only applies under a server (on-demand) adapter, not a static build.'
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function viteSpa(_context) {
|
|
2993
|
+
return {
|
|
2994
|
+
kind: "none",
|
|
2995
|
+
reason: "This is a client-only app \u2014 nothing reads process.env at runtime, so injection does not apply. Read client config through import.meta.env (only VITE_-prefixed vars, which ship to the browser and must not hold secrets). If you add a server later, place the injection there."
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
var SEAMS = {
|
|
2999
|
+
"Next.js": nextjs,
|
|
3000
|
+
SvelteKit: sveltekit,
|
|
3001
|
+
Nuxt: nuxt,
|
|
3002
|
+
Bun: bun,
|
|
3003
|
+
"TanStack Start": tanstack,
|
|
3004
|
+
Astro: astro,
|
|
3005
|
+
Vite: viteSpa
|
|
3006
|
+
};
|
|
3007
|
+
function seamFor(framework, context) {
|
|
3008
|
+
const build = framework === void 0 ? node : SEAMS[framework] ?? node;
|
|
3009
|
+
return build(context);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/commands/init.ts
|
|
2658
3013
|
var CONFIG_FILE = "penv.config.ts";
|
|
2659
3014
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
2660
3015
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -2664,11 +3019,12 @@ var IMPORTS_PREFIX = "#";
|
|
|
2664
3019
|
var PACKAGE_FILE = "package.json";
|
|
2665
3020
|
var DEFAULT_DECISIONS = {
|
|
2666
3021
|
environments: [],
|
|
2667
|
-
|
|
3022
|
+
inject: false,
|
|
3023
|
+
schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
|
|
2668
3024
|
publicPrefixes: [],
|
|
2669
3025
|
alias: DEFAULT_ALIAS
|
|
2670
3026
|
};
|
|
2671
|
-
var NOT_ENVIRONMENTS = [...
|
|
3027
|
+
var NOT_ENVIRONMENTS = [...import_core15.RESERVED_TOKENS, "example", "sample", "template"];
|
|
2672
3028
|
function suggestEnvironments(root) {
|
|
2673
3029
|
let entries;
|
|
2674
3030
|
try {
|
|
@@ -2684,7 +3040,7 @@ function suggestEnvironments(root) {
|
|
|
2684
3040
|
const segments = entry.slice(".env.".length).split(".");
|
|
2685
3041
|
const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
|
|
2686
3042
|
const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
|
|
2687
|
-
if (name === void 0 || !(0,
|
|
3043
|
+
if (name === void 0 || !(0, import_core15.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
|
|
2688
3044
|
continue;
|
|
2689
3045
|
}
|
|
2690
3046
|
found.add(name);
|
|
@@ -2692,10 +3048,10 @@ function suggestEnvironments(root) {
|
|
|
2692
3048
|
return [...found].sort();
|
|
2693
3049
|
}
|
|
2694
3050
|
function emptyFlag(flag) {
|
|
2695
|
-
return new
|
|
3051
|
+
return new import_core15.PenvError(
|
|
2696
3052
|
"INIT_FLAG_EMPTY",
|
|
2697
3053
|
`\`--${flag}\` was given without a value`,
|
|
2698
|
-
flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${
|
|
3054
|
+
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."
|
|
2699
3055
|
);
|
|
2700
3056
|
}
|
|
2701
3057
|
function splitEnvironments(value) {
|
|
@@ -2720,7 +3076,7 @@ function declaredIn(root) {
|
|
|
2720
3076
|
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
2721
3077
|
return void 0;
|
|
2722
3078
|
}
|
|
2723
|
-
return (0,
|
|
3079
|
+
return (0, import_core15.loadConfigFrom)(file);
|
|
2724
3080
|
}
|
|
2725
3081
|
function planInit(root, flags = {}) {
|
|
2726
3082
|
const declared = declaredIn(root);
|
|
@@ -2730,7 +3086,7 @@ function planInit(root, flags = {}) {
|
|
|
2730
3086
|
notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
|
|
2731
3087
|
} else if (detected === void 0) {
|
|
2732
3088
|
notes.push(
|
|
2733
|
-
`No framework detected in package.json \u2014 the schema goes to ${
|
|
3089
|
+
`No framework detected in package.json \u2014 the schema goes to ${import_core15.DEFAULT_SCHEMA_FILE}.`
|
|
2734
3090
|
);
|
|
2735
3091
|
} else {
|
|
2736
3092
|
notes.push(`Detected ${detected.name}.`);
|
|
@@ -2740,12 +3096,12 @@ function planInit(root, flags = {}) {
|
|
|
2740
3096
|
);
|
|
2741
3097
|
}
|
|
2742
3098
|
}
|
|
2743
|
-
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0,
|
|
3099
|
+
const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core15.schemaFileOf)(declared) : detected?.schemaFile ?? import_core15.DEFAULT_SCHEMA_FILE : flags.schema.trim();
|
|
2744
3100
|
if (flags.schema !== void 0) {
|
|
2745
3101
|
if (schemaFile.length === 0) {
|
|
2746
3102
|
throw emptyFlag("schema");
|
|
2747
3103
|
}
|
|
2748
|
-
const error = (0,
|
|
3104
|
+
const error = (0, import_core15.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
|
|
2749
3105
|
if (error !== void 0) {
|
|
2750
3106
|
throw error;
|
|
2751
3107
|
}
|
|
@@ -2755,7 +3111,7 @@ function planInit(root, flags = {}) {
|
|
|
2755
3111
|
throw emptyFlag("alias");
|
|
2756
3112
|
}
|
|
2757
3113
|
if (!ALIAS_NAME.test(alias)) {
|
|
2758
|
-
throw new
|
|
3114
|
+
throw new import_core15.PenvError(
|
|
2759
3115
|
"INIT_ALIAS_INVALID",
|
|
2760
3116
|
`\`${alias}\` is not an alias penv can write`,
|
|
2761
3117
|
`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.`
|
|
@@ -2782,6 +3138,9 @@ function planInit(root, flags = {}) {
|
|
|
2782
3138
|
detected,
|
|
2783
3139
|
decisions: {
|
|
2784
3140
|
environments,
|
|
3141
|
+
// Injection is a choice about the app's needs, not something on disk — so
|
|
3142
|
+
// the plan defaults it off, and only the interactive prompt turns it on.
|
|
3143
|
+
inject: false,
|
|
2785
3144
|
schemaFile,
|
|
2786
3145
|
publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
|
|
2787
3146
|
alias
|
|
@@ -2802,7 +3161,7 @@ function renderPlan(plan2) {
|
|
|
2802
3161
|
rows.push([
|
|
2803
3162
|
` ${out.dim("schemaFile")}`,
|
|
2804
3163
|
plan2.decisions.schemaFile,
|
|
2805
|
-
plan2.decisions.schemaFile ===
|
|
3164
|
+
plan2.decisions.schemaFile === import_core15.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core15.DEFAULT_SCHEMA_FILE})`)
|
|
2806
3165
|
]);
|
|
2807
3166
|
for (const prefix of plan2.decisions.publicPrefixes) {
|
|
2808
3167
|
rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
|
|
@@ -2828,16 +3187,33 @@ async function promptForDecisions(plan2, io) {
|
|
|
2828
3187
|
);
|
|
2829
3188
|
io.write("");
|
|
2830
3189
|
}
|
|
3190
|
+
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
2831
3191
|
const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
|
|
2832
3192
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
2833
3193
|
return void 0;
|
|
2834
3194
|
}
|
|
2835
|
-
return { ...plan2.decisions, environments };
|
|
3195
|
+
return { ...plan2.decisions, environments, inject };
|
|
3196
|
+
}
|
|
3197
|
+
function seamKindFor(plan2) {
|
|
3198
|
+
return seamFor(plan2.detected?.name, {
|
|
3199
|
+
alias: plan2.decisions.alias,
|
|
3200
|
+
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
|
|
3201
|
+
}).kind;
|
|
3202
|
+
}
|
|
3203
|
+
async function askInject(io) {
|
|
3204
|
+
io.write("");
|
|
3205
|
+
io.write("Also inject your validated config into process.env?");
|
|
3206
|
+
io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
|
|
3207
|
+
io.write(' n config stays available only through import { env } from "@env"');
|
|
3208
|
+
const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
|
|
3209
|
+
return answer === "y" || answer === "yes";
|
|
2836
3210
|
}
|
|
2837
3211
|
var EMPTY_SCHEMA_BODY = " // One key per parameter, e.g. `databaseUrl: z.url(),`. Nesting a key nests\n // the parameter: `redis: z.object({ password: z.string() })` is redis/password.";
|
|
2838
3212
|
var DRAFT_HEADER = "// DRAFT \u2014 generated by `penv import` from one sample of each value, and yours\n// to correct. Single-sample inference cannot know that a boolean seen as `true`\n// must also accept `1`/`0`, or that a string is really a URL. penv scaffolds\n// this file once and never regenerates it, so edits here are safe.\n";
|
|
2839
|
-
function renderSchemaModule(fields, draft) {
|
|
3213
|
+
function renderSchemaModule(fields, draft, inject = false) {
|
|
2840
3214
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3215
|
+
const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly (WorkOS, Prisma) instead of importing @env.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid.\n";
|
|
3216
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
2841
3217
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
2842
3218
|
import { load } from "@penvhq/penv";
|
|
2843
3219
|
|
|
@@ -2847,10 +3223,15 @@ export const schema = z.object({
|
|
|
2847
3223
|
${body}
|
|
2848
3224
|
});
|
|
2849
3225
|
|
|
2850
|
-
|
|
2851
|
-
//
|
|
2852
|
-
//
|
|
2853
|
-
|
|
3226
|
+
` + loadComment + loadCall + `
|
|
3227
|
+
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3228
|
+
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3229
|
+
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
3230
|
+
declare module "@penvhq/core" {
|
|
3231
|
+
interface PenvSchemaShape {
|
|
3232
|
+
readonly shape: z.infer<typeof schema>;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2854
3235
|
`;
|
|
2855
3236
|
}
|
|
2856
3237
|
function renderEnvironments(decisions) {
|
|
@@ -2860,14 +3241,14 @@ function renderEnvironments(decisions) {
|
|
|
2860
3241
|
// can read off your codebase, and an environment you do not have is worse
|
|
2861
3242
|
// than one you have not declared yet. Name yours, and give each a provider:
|
|
2862
3243
|
// environments: ["development", "production"],
|
|
2863
|
-
// providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
|
|
3244
|
+
// providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
|
|
2864
3245
|
environments: [],
|
|
2865
3246
|
|
|
2866
3247
|
providers: {},
|
|
2867
3248
|
`;
|
|
2868
3249
|
}
|
|
2869
3250
|
const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
|
|
2870
|
-
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "filesystem" },
|
|
3251
|
+
const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
|
|
2871
3252
|
`).join("");
|
|
2872
3253
|
return `${shared} environments: [${names}],
|
|
2873
3254
|
|
|
@@ -2878,7 +3259,7 @@ ${providers} },
|
|
|
2878
3259
|
}
|
|
2879
3260
|
function renderConfigModule(decisions) {
|
|
2880
3261
|
let body = renderEnvironments(decisions);
|
|
2881
|
-
if (decisions.schemaFile !==
|
|
3262
|
+
if (decisions.schemaFile !== import_core15.DEFAULT_SCHEMA_FILE) {
|
|
2882
3263
|
body += `
|
|
2883
3264
|
// The module that exports the schema. It is yours \u2014 penv scaffolds it once
|
|
2884
3265
|
// and never regenerates it \u2014 so this says where you keep it.
|
|
@@ -2901,7 +3282,7 @@ ${body}});
|
|
|
2901
3282
|
`;
|
|
2902
3283
|
}
|
|
2903
3284
|
function renderGitignore(decisions) {
|
|
2904
|
-
const inside = (0,
|
|
3285
|
+
const inside = (0, import_core15.schemaInsideTree)(configOf(decisions));
|
|
2905
3286
|
const listed = inside === void 0 ? "" : `${inside}, `;
|
|
2906
3287
|
return `# Written by penv. Value files hold configuration values and are never
|
|
2907
3288
|
# committed; only the structure, ${listed}meta, and config are.
|
|
@@ -3046,13 +3427,13 @@ ${objectIndent}${source.slice(close)}`;
|
|
|
3046
3427
|
${entryIndent}${member},${source.slice(open + 1)}`;
|
|
3047
3428
|
}
|
|
3048
3429
|
function shapeError(what, target, alias) {
|
|
3049
|
-
return new
|
|
3430
|
+
return new import_core15.PenvError(
|
|
3050
3431
|
"TSCONFIG_SHAPE",
|
|
3051
3432
|
`penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
|
|
3052
3433
|
`Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
|
|
3053
3434
|
);
|
|
3054
3435
|
}
|
|
3055
|
-
function insertEnvAlias(source, target =
|
|
3436
|
+
function insertEnvAlias(source, target = import_core15.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
|
|
3056
3437
|
const alias = `"${name}": ["${target}"]`;
|
|
3057
3438
|
const root = skipTrivia(source, 0);
|
|
3058
3439
|
if (source.charAt(root) !== "{") {
|
|
@@ -3134,7 +3515,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
3134
3515
|
};
|
|
3135
3516
|
}
|
|
3136
3517
|
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3137
|
-
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
|
|
3518
|
+
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
|
|
3138
3519
|
return {
|
|
3139
3520
|
target: "schema",
|
|
3140
3521
|
action: "created",
|
|
@@ -3211,14 +3592,56 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3211
3592
|
text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
|
|
3212
3593
|
};
|
|
3213
3594
|
}
|
|
3595
|
+
function srcDirOf(root) {
|
|
3596
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path6.join)(root, "src")) ? "src/" : "";
|
|
3597
|
+
}
|
|
3598
|
+
function writeSeam(root, decisions = DEFAULT_DECISIONS) {
|
|
3599
|
+
if (!decisions.inject) {
|
|
3600
|
+
return void 0;
|
|
3601
|
+
}
|
|
3602
|
+
const framework = detectFramework(root)?.name;
|
|
3603
|
+
const seam = seamFor(framework, { alias: decisions.alias, srcDir: srcDirOf(root) });
|
|
3604
|
+
if (seam.kind === "none") {
|
|
3605
|
+
return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
|
|
3606
|
+
}
|
|
3607
|
+
if (seam.kind === "instruct") {
|
|
3608
|
+
return {
|
|
3609
|
+
target: "seam",
|
|
3610
|
+
action: "info",
|
|
3611
|
+
text: "Place the injection seam",
|
|
3612
|
+
note: seam.instruction
|
|
3613
|
+
};
|
|
3614
|
+
}
|
|
3615
|
+
const file = (0, import_node_path6.join)(root, ...seam.file.split("/"));
|
|
3616
|
+
const notes = seam.notes.length === 0 ? "" : `
|
|
3617
|
+
${seam.notes.map((n) => ` ${n}`).join("\n")}`;
|
|
3618
|
+
if ((0, import_node_fs3.existsSync)(file)) {
|
|
3619
|
+
return {
|
|
3620
|
+
target: "seam",
|
|
3621
|
+
action: "info",
|
|
3622
|
+
text: `Add the injection seam to ${seam.file}`,
|
|
3623
|
+
note: `${seam.ifPresent}${notes}`
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3627
|
+
(0, import_node_fs3.writeFileSync)(file, seam.content, "utf8");
|
|
3628
|
+
return {
|
|
3629
|
+
target: "seam",
|
|
3630
|
+
action: "created",
|
|
3631
|
+
text: `Wrote ${seam.file} (runs the injection before your app)`,
|
|
3632
|
+
...notes === "" ? {} : { note: notes.trimStart() }
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3214
3635
|
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
3215
|
-
|
|
3636
|
+
const steps = [
|
|
3216
3637
|
ensurePenvDir(root),
|
|
3217
3638
|
writeSchemaFile(root, fields, draft, decisions),
|
|
3218
3639
|
writeConfigFile(root, decisions),
|
|
3219
3640
|
writeTsconfigAlias(root, decisions),
|
|
3220
3641
|
writeGitignore(root, decisions)
|
|
3221
3642
|
];
|
|
3643
|
+
const seam = writeSeam(root, decisions);
|
|
3644
|
+
return seam === void 0 ? steps : [...steps, seam];
|
|
3222
3645
|
}
|
|
3223
3646
|
function runInit(options) {
|
|
3224
3647
|
const root = (0, import_node_path6.resolve)(options.cwd);
|
|
@@ -3227,7 +3650,7 @@ function runInit(options) {
|
|
|
3227
3650
|
}
|
|
3228
3651
|
function renderInit(result2) {
|
|
3229
3652
|
const steps = result2.steps.map((step) => {
|
|
3230
|
-
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
3653
|
+
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3231
3654
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3232
3655
|
});
|
|
3233
3656
|
return [
|
|
@@ -3261,7 +3684,7 @@ var initCommand = (0, import_citty9.defineCommand)({
|
|
|
3261
3684
|
},
|
|
3262
3685
|
schema: {
|
|
3263
3686
|
type: "string",
|
|
3264
|
-
description: `Where the schema module goes, e.g. src/env.ts (default: ${
|
|
3687
|
+
description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core15.DEFAULT_SCHEMA_FILE})`
|
|
3265
3688
|
},
|
|
3266
3689
|
alias: {
|
|
3267
3690
|
type: "string",
|
|
@@ -3315,7 +3738,7 @@ function inferType(value) {
|
|
|
3315
3738
|
}
|
|
3316
3739
|
function draftFields(entries) {
|
|
3317
3740
|
return entries.map((entry) => {
|
|
3318
|
-
const key = (0,
|
|
3741
|
+
const key = (0, import_core16.accessPath)((0, import_core16.refFromVariable)(entry.key)).join(".");
|
|
3319
3742
|
return {
|
|
3320
3743
|
key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
|
|
3321
3744
|
type: inferType(entry.value)
|
|
@@ -3326,33 +3749,33 @@ function assertImportable(ref, variable) {
|
|
|
3326
3749
|
if (!ref.name.includes(".")) {
|
|
3327
3750
|
return;
|
|
3328
3751
|
}
|
|
3329
|
-
throw new
|
|
3752
|
+
throw new import_core16.PenvError(
|
|
3330
3753
|
"IMPORT_UNPARSEABLE_NAME",
|
|
3331
3754
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
|
|
3332
3755
|
`Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
|
|
3333
3756
|
);
|
|
3334
3757
|
}
|
|
3335
3758
|
function assertNotReserved(ref, variable, where, config) {
|
|
3336
|
-
if ((0,
|
|
3337
|
-
throw new
|
|
3759
|
+
if ((0, import_core16.isReservedToken)(ref.name, config)) {
|
|
3760
|
+
throw new import_core16.ReservedTokenError("parameter", variable, where);
|
|
3338
3761
|
}
|
|
3339
3762
|
}
|
|
3340
3763
|
function assertRoundTrips(ref, variable, config) {
|
|
3341
|
-
if ((0,
|
|
3764
|
+
if ((0, import_core16.roundTripsCleanly)(variable)) {
|
|
3342
3765
|
return;
|
|
3343
3766
|
}
|
|
3344
|
-
if ((0,
|
|
3767
|
+
if ((0, import_core16.variableName)(ref, config) === variable) {
|
|
3345
3768
|
return;
|
|
3346
3769
|
}
|
|
3347
|
-
const generated = (0,
|
|
3348
|
-
throw new
|
|
3770
|
+
const generated = (0, import_core16.variableName)(ref, config);
|
|
3771
|
+
throw new import_core16.PenvError(
|
|
3349
3772
|
"IMPORT_LOSSY_NAME",
|
|
3350
3773
|
`The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
|
|
3351
|
-
`\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`
|
|
3774
|
+
`\`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.`
|
|
3352
3775
|
);
|
|
3353
3776
|
}
|
|
3354
3777
|
function collisionsIn(refs, config) {
|
|
3355
|
-
const errors = (0,
|
|
3778
|
+
const errors = (0, import_core16.checkNameCollisions)(refs, config);
|
|
3356
3779
|
const first = errors[0];
|
|
3357
3780
|
if (first !== void 0) {
|
|
3358
3781
|
throw first;
|
|
@@ -3362,7 +3785,7 @@ function assertDeclared(segment, config) {
|
|
|
3362
3785
|
if (config.environments.includes(segment)) {
|
|
3363
3786
|
return segment;
|
|
3364
3787
|
}
|
|
3365
|
-
throw new
|
|
3788
|
+
throw new import_core16.UnknownEnvironmentError(segment, config.environments);
|
|
3366
3789
|
}
|
|
3367
3790
|
function scopeFromFilename(file, config) {
|
|
3368
3791
|
const name = (0, import_node_path7.basename)(file);
|
|
@@ -3387,13 +3810,13 @@ function scopeFromFilename(file, config) {
|
|
|
3387
3810
|
return { kind: "environment-local", environment: assertDeclared(first, config) };
|
|
3388
3811
|
}
|
|
3389
3812
|
if (first === LOCAL && rest.length === 2) {
|
|
3390
|
-
throw new
|
|
3813
|
+
throw new import_core16.FilenameGrammarError(
|
|
3391
3814
|
name,
|
|
3392
3815
|
"`local` precedes the environment segment",
|
|
3393
3816
|
`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.`
|
|
3394
3817
|
);
|
|
3395
3818
|
}
|
|
3396
|
-
throw new
|
|
3819
|
+
throw new import_core16.FilenameGrammarError(
|
|
3397
3820
|
name,
|
|
3398
3821
|
`\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
|
|
3399
3822
|
"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."
|
|
@@ -3408,7 +3831,7 @@ function environmentOf(scope) {
|
|
|
3408
3831
|
case "local":
|
|
3409
3832
|
return void 0;
|
|
3410
3833
|
default:
|
|
3411
|
-
return (0,
|
|
3834
|
+
return (0, import_core16.assertNever)(scope, "scope");
|
|
3412
3835
|
}
|
|
3413
3836
|
}
|
|
3414
3837
|
function scopeWithEnvironment(scope, environment) {
|
|
@@ -3421,7 +3844,7 @@ function scopeWithEnvironment(scope, environment) {
|
|
|
3421
3844
|
case "environment-local":
|
|
3422
3845
|
return scope;
|
|
3423
3846
|
default:
|
|
3424
|
-
return (0,
|
|
3847
|
+
return (0, import_core16.assertNever)(scope, "scope");
|
|
3425
3848
|
}
|
|
3426
3849
|
}
|
|
3427
3850
|
function explicitEnvironment(options, source, config) {
|
|
@@ -3429,7 +3852,7 @@ function explicitEnvironment(options, source, config) {
|
|
|
3429
3852
|
if (value.length > 0) {
|
|
3430
3853
|
return value;
|
|
3431
3854
|
}
|
|
3432
|
-
throw new
|
|
3855
|
+
throw new import_core16.PenvError(
|
|
3433
3856
|
"IMPORT_ENV_FLAG_EMPTY",
|
|
3434
3857
|
`\`--env\` for the import of ${source} names no environment`,
|
|
3435
3858
|
`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.`
|
|
@@ -3439,7 +3862,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
|
|
|
3439
3862
|
if (derived === void 0 || explicit === void 0 || derived === explicit) {
|
|
3440
3863
|
return;
|
|
3441
3864
|
}
|
|
3442
|
-
throw new
|
|
3865
|
+
throw new import_core16.PenvError(
|
|
3443
3866
|
"IMPORT_ENV_CONFLICT",
|
|
3444
3867
|
`The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
|
|
3445
3868
|
`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.`
|
|
@@ -3460,15 +3883,18 @@ function environmentNamed(file, explicit) {
|
|
|
3460
3883
|
function decisionsOf(config, cwd) {
|
|
3461
3884
|
return {
|
|
3462
3885
|
environments: config.environments,
|
|
3463
|
-
|
|
3886
|
+
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
3887
|
+
// choice, so it is not turned on here.
|
|
3888
|
+
inject: false,
|
|
3889
|
+
schemaFile: (0, import_core16.schemaFileOf)(config),
|
|
3464
3890
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3465
3891
|
alias: detectAlias(cwd)
|
|
3466
3892
|
};
|
|
3467
3893
|
}
|
|
3468
3894
|
function configInEffect(cwd, environment) {
|
|
3469
|
-
const existing = (0,
|
|
3895
|
+
const existing = (0, import_core16.findConfigFile)(cwd);
|
|
3470
3896
|
if (existing !== void 0) {
|
|
3471
|
-
const config = (0,
|
|
3897
|
+
const config = (0, import_core16.loadConfigFrom)(existing);
|
|
3472
3898
|
return { config, decisions: decisionsOf(config, cwd) };
|
|
3473
3899
|
}
|
|
3474
3900
|
const planned = planInit(cwd).decisions;
|
|
@@ -3483,13 +3909,13 @@ function importDotenv(options) {
|
|
|
3483
3909
|
const cwd = (0, import_node_path7.resolve)(options.cwd);
|
|
3484
3910
|
const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
|
|
3485
3911
|
if (!(0, import_node_fs4.existsSync)(file)) {
|
|
3486
|
-
throw new
|
|
3912
|
+
throw new import_core16.PenvError(
|
|
3487
3913
|
"IMPORT_FILE_MISSING",
|
|
3488
3914
|
`There is no file at ${file} to import`,
|
|
3489
3915
|
"Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
|
|
3490
3916
|
);
|
|
3491
3917
|
}
|
|
3492
|
-
const parsed = (0,
|
|
3918
|
+
const parsed = (0, import_core16.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
|
|
3493
3919
|
const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
|
|
3494
3920
|
const source = displayPath2(cwd, file);
|
|
3495
3921
|
const named = scopeFromFilename(file, config);
|
|
@@ -3497,10 +3923,10 @@ function importDotenv(options) {
|
|
|
3497
3923
|
const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
|
|
3498
3924
|
assertEnvironmentAgrees(derived, explicit, source);
|
|
3499
3925
|
const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
|
|
3500
|
-
const environment = (0,
|
|
3926
|
+
const environment = (0, import_core16.lookupEnvironment)(config, explicit ?? derived);
|
|
3501
3927
|
const refs = [];
|
|
3502
3928
|
for (const entry of parsed.entries) {
|
|
3503
|
-
const ref = (0,
|
|
3929
|
+
const ref = (0, import_core16.refFromVariable)(entry.key);
|
|
3504
3930
|
assertImportable(ref, entry.key);
|
|
3505
3931
|
assertNotReserved(ref, entry.key, source, config);
|
|
3506
3932
|
assertRoundTrips(ref, entry.key, config);
|
|
@@ -3628,7 +4054,7 @@ var importCommand = (0, import_citty10.defineCommand)({
|
|
|
3628
4054
|
|
|
3629
4055
|
// src/commands/key.ts
|
|
3630
4056
|
var import_node_crypto = require("crypto");
|
|
3631
|
-
var
|
|
4057
|
+
var import_core17 = require("@penvhq/core");
|
|
3632
4058
|
var import_citty11 = require("citty");
|
|
3633
4059
|
|
|
3634
4060
|
// src/keychain.ts
|
|
@@ -3662,35 +4088,35 @@ function runKeyCreate(options) {
|
|
|
3662
4088
|
const environment = targetEnvironment(project, options.environment);
|
|
3663
4089
|
const declared = project.config.keys?.[environment];
|
|
3664
4090
|
if (declared === void 0) {
|
|
3665
|
-
throw new
|
|
4091
|
+
throw new import_core17.PenvError(
|
|
3666
4092
|
"KEY_SOURCE_UNDECLARED",
|
|
3667
4093
|
`Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
|
|
3668
4094
|
`Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
|
|
3669
4095
|
);
|
|
3670
4096
|
}
|
|
3671
|
-
const key = (0, import_node_crypto.randomBytes)(
|
|
4097
|
+
const key = (0, import_node_crypto.randomBytes)(import_core17.KEY_BYTES).toString("base64");
|
|
3672
4098
|
if (declared.source === "keychain") {
|
|
3673
4099
|
const keychain = options.keychain ?? defaultKeychain;
|
|
3674
4100
|
if (options.force !== true) {
|
|
3675
4101
|
let existing;
|
|
3676
4102
|
try {
|
|
3677
|
-
existing = keychain.getPassword(
|
|
4103
|
+
existing = keychain.getPassword(import_core17.KEYCHAIN_SERVICE, declared.id);
|
|
3678
4104
|
} catch (cause) {
|
|
3679
|
-
throw new
|
|
4105
|
+
throw new import_core17.PenvError(
|
|
3680
4106
|
"KEYCHAIN_UNAVAILABLE",
|
|
3681
4107
|
`penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
|
|
3682
4108
|
`Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
3683
4109
|
);
|
|
3684
4110
|
}
|
|
3685
4111
|
if (existing !== null) {
|
|
3686
|
-
throw new
|
|
4112
|
+
throw new import_core17.PenvError(
|
|
3687
4113
|
"KEY_EXISTS",
|
|
3688
4114
|
`Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
|
|
3689
4115
|
"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."
|
|
3690
4116
|
);
|
|
3691
4117
|
}
|
|
3692
4118
|
}
|
|
3693
|
-
keychain.setPassword(
|
|
4119
|
+
keychain.setPassword(import_core17.KEYCHAIN_SERVICE, declared.id, key);
|
|
3694
4120
|
return { source: "keychain", environment, id: declared.id };
|
|
3695
4121
|
}
|
|
3696
4122
|
return {
|
|
@@ -3758,7 +4184,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
|
|
|
3758
4184
|
});
|
|
3759
4185
|
|
|
3760
4186
|
// src/commands/list.ts
|
|
3761
|
-
var
|
|
4187
|
+
var import_core18 = require("@penvhq/core");
|
|
3762
4188
|
var import_citty12 = require("citty");
|
|
3763
4189
|
function scopeLabel2(scope) {
|
|
3764
4190
|
switch (scope.kind) {
|
|
@@ -3771,7 +4197,7 @@ function scopeLabel2(scope) {
|
|
|
3771
4197
|
case "unscoped":
|
|
3772
4198
|
return "default";
|
|
3773
4199
|
default:
|
|
3774
|
-
return (0,
|
|
4200
|
+
return (0, import_core18.assertNever)(scope, "scope");
|
|
3775
4201
|
}
|
|
3776
4202
|
}
|
|
3777
4203
|
async function runList(options) {
|
|
@@ -3779,12 +4205,12 @@ async function runList(options) {
|
|
|
3779
4205
|
const environment = targetEnvironment(project, options.environment);
|
|
3780
4206
|
const keys = keySourceFor(project, environment);
|
|
3781
4207
|
const parameters = [];
|
|
3782
|
-
for (const resolution of await (0,
|
|
4208
|
+
for (const resolution of await (0, import_core18.resolveAll)(environment, project.provider, keys)) {
|
|
3783
4209
|
const winner = resolution.winner;
|
|
3784
4210
|
const scope = winner?.file.scope;
|
|
3785
4211
|
parameters.push({
|
|
3786
4212
|
parameter: resolution.parameter,
|
|
3787
|
-
variable: (0,
|
|
4213
|
+
variable: (0, import_core18.variableName)(resolution.ref, project.config),
|
|
3788
4214
|
scope: scope === void 0 ? "absent" : scopeLabel2(scope),
|
|
3789
4215
|
location: winner?.location,
|
|
3790
4216
|
encrypted: winner?.file.encrypted === true,
|
|
@@ -3837,7 +4263,7 @@ var listCommand = (0, import_citty12.defineCommand)({
|
|
|
3837
4263
|
});
|
|
3838
4264
|
|
|
3839
4265
|
// src/commands/mv.ts
|
|
3840
|
-
var
|
|
4266
|
+
var import_core19 = require("@penvhq/core");
|
|
3841
4267
|
var import_citty13 = require("citty");
|
|
3842
4268
|
function environmentOf2(file) {
|
|
3843
4269
|
const scope = file.scope;
|
|
@@ -3853,39 +4279,39 @@ async function planFile(project, source, target, parameter) {
|
|
|
3853
4279
|
}
|
|
3854
4280
|
const environment = environmentOf2(source);
|
|
3855
4281
|
if (environment === void 0) {
|
|
3856
|
-
throw new
|
|
4282
|
+
throw new import_core19.PenvError(
|
|
3857
4283
|
"SECRET_SCOPE_AMBIGUOUS",
|
|
3858
|
-
`${PENV_DIR}/${(0,
|
|
4284
|
+
`${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`,
|
|
3859
4285
|
"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."
|
|
3860
4286
|
);
|
|
3861
4287
|
}
|
|
3862
4288
|
const keys = keySourceFor(project, environment);
|
|
3863
|
-
const opened = (0,
|
|
4289
|
+
const opened = (0, import_core19.openValue)(source, stored, keys);
|
|
3864
4290
|
if (opened.kind === "failed") {
|
|
3865
|
-
throw new
|
|
4291
|
+
throw new import_core19.PenvError(
|
|
3866
4292
|
"VALUE_UNDECRYPTABLE",
|
|
3867
|
-
`${PENV_DIR}/${(0,
|
|
4293
|
+
`${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
|
|
3868
4294
|
"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."
|
|
3869
4295
|
);
|
|
3870
4296
|
}
|
|
3871
4297
|
return {
|
|
3872
4298
|
source,
|
|
3873
4299
|
target,
|
|
3874
|
-
contents: (0,
|
|
4300
|
+
contents: (0, import_core19.sealValue)(target, opened.value, keys, parameter, environment),
|
|
3875
4301
|
resealed: true
|
|
3876
4302
|
};
|
|
3877
4303
|
}
|
|
3878
4304
|
function filesOf(all, ref) {
|
|
3879
|
-
const id = (0,
|
|
3880
|
-
return all.filter((file) => (0,
|
|
4305
|
+
const id = (0, import_core19.parameterId)(ref);
|
|
4306
|
+
return all.filter((file) => (0, import_core19.parameterId)(file) === id);
|
|
3881
4307
|
}
|
|
3882
4308
|
async function runMove(options) {
|
|
3883
4309
|
const project = openProject(options.cwd);
|
|
3884
4310
|
const from = refFromKey(options.from, project.config);
|
|
3885
4311
|
assertWritableKey(options.to);
|
|
3886
4312
|
const to = refFromKey(options.to, project.config);
|
|
3887
|
-
if ((0,
|
|
3888
|
-
throw new
|
|
4313
|
+
if ((0, import_core19.parameterId)(from) === (0, import_core19.parameterId)(to)) {
|
|
4314
|
+
throw new import_core19.PenvError(
|
|
3889
4315
|
"PARAMETER_UNCHANGED",
|
|
3890
4316
|
`\`${options.from}\` and \`${options.to}\` are the same parameter`,
|
|
3891
4317
|
"Name a different destination, e.g. `penv mv redis-password redis/password`."
|
|
@@ -3895,24 +4321,24 @@ async function runMove(options) {
|
|
|
3895
4321
|
const sources = filesOf(all, from);
|
|
3896
4322
|
const meta = await project.provider.readMeta(from);
|
|
3897
4323
|
if (sources.length === 0 && meta === void 0) {
|
|
3898
|
-
throw new
|
|
4324
|
+
throw new import_core19.PenvError(
|
|
3899
4325
|
"PARAMETER_ABSENT",
|
|
3900
|
-
`Parameter ${(0,
|
|
4326
|
+
`Parameter ${(0, import_core19.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
|
|
3901
4327
|
`\`penv list\` shows every parameter penv holds.`
|
|
3902
4328
|
);
|
|
3903
4329
|
}
|
|
3904
4330
|
const occupied2 = filesOf(all, to);
|
|
3905
4331
|
if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
|
|
3906
|
-
throw new
|
|
4332
|
+
throw new import_core19.PenvError(
|
|
3907
4333
|
"PARAMETER_EXISTS",
|
|
3908
|
-
`Parameter ${(0,
|
|
3909
|
-
`Remove or rename ${(0,
|
|
4334
|
+
`Parameter ${(0, import_core19.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
|
|
4335
|
+
`Remove or rename ${(0, import_core19.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
|
|
3910
4336
|
);
|
|
3911
4337
|
}
|
|
3912
4338
|
const planned = [];
|
|
3913
4339
|
for (const source of sources) {
|
|
3914
4340
|
const target = { ...source, namespace: to.namespace, name: to.name };
|
|
3915
|
-
const one = await planFile(project, source, target, (0,
|
|
4341
|
+
const one = await planFile(project, source, target, (0, import_core19.parameterId)(to));
|
|
3916
4342
|
if (one !== void 0) {
|
|
3917
4343
|
planned.push(one);
|
|
3918
4344
|
}
|
|
@@ -3930,18 +4356,18 @@ async function runMove(options) {
|
|
|
3930
4356
|
await project.provider.removeMeta(from);
|
|
3931
4357
|
}
|
|
3932
4358
|
return {
|
|
3933
|
-
from: (0,
|
|
3934
|
-
to: (0,
|
|
4359
|
+
from: (0, import_core19.parameterId)(from),
|
|
4360
|
+
to: (0, import_core19.parameterId)(to),
|
|
3935
4361
|
files: planned.map((file) => ({
|
|
3936
|
-
from: (0,
|
|
3937
|
-
to: (0,
|
|
4362
|
+
from: (0, import_core19.formatValueFile)(file.source),
|
|
4363
|
+
to: (0, import_core19.formatValueFile)(file.target),
|
|
3938
4364
|
resealed: file.resealed
|
|
3939
4365
|
})),
|
|
3940
4366
|
// The meta's path, not the parameter's dotted id: `redis.password` is what
|
|
3941
4367
|
// the schema calls it and `redis/password.json` is the file, and a report
|
|
3942
4368
|
// that printed the first while moving the second names no file on disk.
|
|
3943
|
-
meta: meta === void 0 ? void 0 : (0,
|
|
3944
|
-
schema: { was: (0,
|
|
4369
|
+
meta: meta === void 0 ? void 0 : (0, import_core19.formatMetaFile)({ ...to, format: "json" }),
|
|
4370
|
+
schema: { was: (0, import_core19.accessPath)(from).join("."), now: (0, import_core19.accessPath)(to).join(".") }
|
|
3945
4371
|
};
|
|
3946
4372
|
}
|
|
3947
4373
|
function renderMove(result2) {
|
|
@@ -3985,14 +4411,45 @@ var mvCommand = (0, import_citty13.defineCommand)({
|
|
|
3985
4411
|
});
|
|
3986
4412
|
|
|
3987
4413
|
// src/commands/pull.ts
|
|
4414
|
+
var import_core20 = require("@penvhq/core");
|
|
3988
4415
|
var import_citty14 = require("citty");
|
|
4416
|
+
async function pullProjection(project, source, environment) {
|
|
4417
|
+
const tree = localTree(project);
|
|
4418
|
+
const names = /* @__PURE__ */ new Set();
|
|
4419
|
+
for (const secret of await source.list({ kind: "repository" })) {
|
|
4420
|
+
names.add(secret.name);
|
|
4421
|
+
}
|
|
4422
|
+
for (const secret of await source.list({ kind: "environment", environment })) {
|
|
4423
|
+
names.add(secret.name);
|
|
4424
|
+
}
|
|
4425
|
+
let meta = 0;
|
|
4426
|
+
for (const name of [...names].sort()) {
|
|
4427
|
+
const ref = (0, import_core20.refFromVariable)(name);
|
|
4428
|
+
if (tree.readMetaSync(ref) === void 0) {
|
|
4429
|
+
tree.writeMetaSync(ref, {});
|
|
4430
|
+
meta += 1;
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
return {
|
|
4434
|
+
environment,
|
|
4435
|
+
source: source.type,
|
|
4436
|
+
localSource: false,
|
|
4437
|
+
values: 0,
|
|
4438
|
+
meta,
|
|
4439
|
+
refs: names.size,
|
|
4440
|
+
valuesUnreadable: true
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
3989
4443
|
async function runPull(options) {
|
|
3990
4444
|
const project = openProject(options.cwd);
|
|
3991
|
-
const environment = targetEnvironment(project, options.environment);
|
|
3992
|
-
const source = await sourceProviderFor(project, environment);
|
|
4445
|
+
const environment = targetEnvironment(project, options.environment, options.envFlags);
|
|
4446
|
+
const source = options.source ?? await sourceProviderFor(project, environment);
|
|
3993
4447
|
if (source.type === LOCAL_TREE_TYPE) {
|
|
3994
4448
|
return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
|
|
3995
4449
|
}
|
|
4450
|
+
if ((0, import_core20.holdsProjection)(source)) {
|
|
4451
|
+
return pullProjection(project, source, environment);
|
|
4452
|
+
}
|
|
3996
4453
|
const tree = localTree(project);
|
|
3997
4454
|
const files = await source.list();
|
|
3998
4455
|
let values = 0;
|
|
@@ -4027,6 +4484,22 @@ function renderPull(result2) {
|
|
|
4027
4484
|
}
|
|
4028
4485
|
]);
|
|
4029
4486
|
}
|
|
4487
|
+
if (result2.valuesUnreadable === true) {
|
|
4488
|
+
return formatRows([
|
|
4489
|
+
{
|
|
4490
|
+
glyph: CHECK,
|
|
4491
|
+
label: "Pulled",
|
|
4492
|
+
subject: `${result2.refs} ${result2.refs === 1 ? "parameter name" : "parameter names"}`,
|
|
4493
|
+
detail: `from ${result2.source} for environment ${result2.environment}`
|
|
4494
|
+
},
|
|
4495
|
+
{
|
|
4496
|
+
glyph: WARN,
|
|
4497
|
+
label: "Values not readable",
|
|
4498
|
+
subject: "this destination never returns a secret's value",
|
|
4499
|
+
detail: "fill them locally with `penv set` or `penv fill` \u2014 `penv validate` names every gap"
|
|
4500
|
+
}
|
|
4501
|
+
]);
|
|
4502
|
+
}
|
|
4030
4503
|
return formatRows([
|
|
4031
4504
|
{
|
|
4032
4505
|
glyph: CHECK,
|
|
@@ -4048,13 +4521,17 @@ var pullCommand = (0, import_citty14.defineCommand)({
|
|
|
4048
4521
|
description: "Materialise the local .penv tree from an environment's source-of-truth provider"
|
|
4049
4522
|
},
|
|
4050
4523
|
args: {
|
|
4051
|
-
env: {
|
|
4524
|
+
env: {
|
|
4525
|
+
type: "string",
|
|
4526
|
+
description: "The environment to pull (or pass it as a bare flag: --production)"
|
|
4527
|
+
}
|
|
4052
4528
|
},
|
|
4053
4529
|
run({ args }) {
|
|
4054
4530
|
return guard(async () => {
|
|
4055
4531
|
const result2 = await runPull({
|
|
4056
4532
|
cwd: process.cwd(),
|
|
4057
|
-
...args.env === void 0 ? {} : { environment: args.env }
|
|
4533
|
+
...args.env === void 0 ? {} : { environment: args.env },
|
|
4534
|
+
envFlags: shorthandCandidates(args, ["env"])
|
|
4058
4535
|
});
|
|
4059
4536
|
write(renderPull(result2));
|
|
4060
4537
|
});
|
|
@@ -4062,7 +4539,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
|
|
|
4062
4539
|
});
|
|
4063
4540
|
|
|
4064
4541
|
// src/commands/remove.ts
|
|
4065
|
-
var
|
|
4542
|
+
var import_core21 = require("@penvhq/core");
|
|
4066
4543
|
var import_citty15 = require("citty");
|
|
4067
4544
|
async function runRemove(options) {
|
|
4068
4545
|
const project = openProject(options.cwd);
|
|
@@ -4080,12 +4557,12 @@ async function runRemove(options) {
|
|
|
4080
4557
|
continue;
|
|
4081
4558
|
}
|
|
4082
4559
|
await project.provider.remove(file);
|
|
4083
|
-
removed.push((0,
|
|
4560
|
+
removed.push((0, import_core21.formatValueFile)(file));
|
|
4084
4561
|
}
|
|
4085
4562
|
return {
|
|
4086
4563
|
parameter: options.key,
|
|
4087
4564
|
removed,
|
|
4088
|
-
considered: files.map((file) => (0,
|
|
4565
|
+
considered: files.map((file) => (0, import_core21.formatValueFile)(file))
|
|
4089
4566
|
};
|
|
4090
4567
|
}
|
|
4091
4568
|
function renderRemove(result2) {
|
|
@@ -4134,7 +4611,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
|
|
|
4134
4611
|
});
|
|
4135
4612
|
|
|
4136
4613
|
// src/commands/rotate.ts
|
|
4137
|
-
var
|
|
4614
|
+
var import_core22 = require("@penvhq/core");
|
|
4138
4615
|
var import_citty16 = require("citty");
|
|
4139
4616
|
function rotatingFile(ref, environment) {
|
|
4140
4617
|
return {
|
|
@@ -4160,7 +4637,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
|
|
|
4160
4637
|
}
|
|
4161
4638
|
function requireNewValue(value, phase, key) {
|
|
4162
4639
|
if (value === void 0) {
|
|
4163
|
-
throw new
|
|
4640
|
+
throw new import_core22.PenvError(
|
|
4164
4641
|
"ROTATION_NO_VALUE",
|
|
4165
4642
|
`A ${phase} rotation of ${key} writes a new value, and none was given`,
|
|
4166
4643
|
"Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
|
|
@@ -4169,8 +4646,8 @@ function requireNewValue(value, phase, key) {
|
|
|
4169
4646
|
return value;
|
|
4170
4647
|
}
|
|
4171
4648
|
function requireRetaining(provider, environment) {
|
|
4172
|
-
if (!(0,
|
|
4173
|
-
throw new
|
|
4649
|
+
if (!(0, import_core22.retainsPrevious)(provider)) {
|
|
4650
|
+
throw new import_core22.PenvError(
|
|
4174
4651
|
"ROTATION_NOT_RETAINING",
|
|
4175
4652
|
`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`,
|
|
4176
4653
|
"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."
|
|
@@ -4182,12 +4659,20 @@ async function runRotate(options) {
|
|
|
4182
4659
|
const project = openProject(options.cwd);
|
|
4183
4660
|
const environment = targetEnvironment(project, options.environment);
|
|
4184
4661
|
const ref = refFromKey(options.key, project.config);
|
|
4185
|
-
const
|
|
4662
|
+
const source = await sourceProviderFor(project, environment);
|
|
4663
|
+
if (!(0, import_core22.holdsRecords)(source)) {
|
|
4664
|
+
throw new import_core22.PenvError(
|
|
4665
|
+
"ROTATION_NOT_RECORDS",
|
|
4666
|
+
`Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
|
|
4667
|
+
"Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
|
|
4668
|
+
);
|
|
4669
|
+
}
|
|
4670
|
+
const provider = source;
|
|
4186
4671
|
const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4187
4672
|
const before = await provider.readMeta(ref);
|
|
4188
|
-
const { mechanism } = (0,
|
|
4673
|
+
const { mechanism } = (0, import_core22.rotationOf)(before, environment);
|
|
4189
4674
|
if (mechanism === void 0) {
|
|
4190
|
-
throw new
|
|
4675
|
+
throw new import_core22.PenvError(
|
|
4191
4676
|
"ROTATION_NO_MECHANISM",
|
|
4192
4677
|
`Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
|
|
4193
4678
|
'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.'
|
|
@@ -4197,7 +4682,7 @@ async function runRotate(options) {
|
|
|
4197
4682
|
const complete = options.complete === true;
|
|
4198
4683
|
if (mechanism === "atomic-cutover") {
|
|
4199
4684
|
if (begin || complete) {
|
|
4200
|
-
throw new
|
|
4685
|
+
throw new import_core22.PenvError(
|
|
4201
4686
|
"ROTATION_MECHANISM_MISMATCH",
|
|
4202
4687
|
`Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
|
|
4203
4688
|
"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."
|
|
@@ -4205,13 +4690,13 @@ async function runRotate(options) {
|
|
|
4205
4690
|
}
|
|
4206
4691
|
const value = requireNewValue(options.value, "cutover", options.key);
|
|
4207
4692
|
await writeRotatedValue(project, provider, ref, environment, value);
|
|
4208
|
-
const after2 = (0,
|
|
4693
|
+
const after2 = (0, import_core22.completeRotation)(before, environment, nowIso);
|
|
4209
4694
|
await provider.writeMeta(ref, after2);
|
|
4210
4695
|
return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
|
|
4211
4696
|
}
|
|
4212
4697
|
const retaining = requireRetaining(provider, environment);
|
|
4213
4698
|
if (begin === complete) {
|
|
4214
|
-
throw new
|
|
4699
|
+
throw new import_core22.PenvError(
|
|
4215
4700
|
"ROTATION_PHASE_REQUIRED",
|
|
4216
4701
|
`A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
|
|
4217
4702
|
"`--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."
|
|
@@ -4220,16 +4705,16 @@ async function runRotate(options) {
|
|
|
4220
4705
|
if (begin) {
|
|
4221
4706
|
const value = requireNewValue(options.value, "begin", options.key);
|
|
4222
4707
|
await writeRotatedValue(project, retaining, ref, environment, value);
|
|
4223
|
-
const after2 = (0,
|
|
4708
|
+
const after2 = (0, import_core22.beginRotation)(before, environment, nowIso);
|
|
4224
4709
|
await retaining.writeMeta(ref, after2);
|
|
4225
4710
|
return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
|
|
4226
4711
|
}
|
|
4227
|
-
const after = (0,
|
|
4712
|
+
const after = (0, import_core22.completeRotation)(before, environment, nowIso);
|
|
4228
4713
|
await retaining.writeMeta(ref, after);
|
|
4229
4714
|
return result(ref, environment, mechanism, "complete", retaining.type, false, after);
|
|
4230
4715
|
}
|
|
4231
4716
|
function result(ref, environment, mechanism, phase, source, wroteValue, after) {
|
|
4232
|
-
const { state, rotatingSince, lastRotated } = (0,
|
|
4717
|
+
const { state, rotatingSince, lastRotated } = (0, import_core22.rotationOf)(after, environment);
|
|
4233
4718
|
return {
|
|
4234
4719
|
parameter: [...ref.namespace, ref.name].join("/"),
|
|
4235
4720
|
environment,
|
|
@@ -4316,7 +4801,7 @@ var rotateCommand = (0, import_citty16.defineCommand)({
|
|
|
4316
4801
|
// src/commands/watch.ts
|
|
4317
4802
|
var import_node_fs5 = require("fs");
|
|
4318
4803
|
var import_node_path8 = require("path");
|
|
4319
|
-
var
|
|
4804
|
+
var import_core23 = require("@penvhq/core");
|
|
4320
4805
|
var import_citty17 = require("citty");
|
|
4321
4806
|
var DEBOUNCE_MS = 100;
|
|
4322
4807
|
function runWatch(options) {
|
|
@@ -4449,8 +4934,8 @@ function runWatch(options) {
|
|
|
4449
4934
|
}
|
|
4450
4935
|
addWatcher(project.penvDir, true);
|
|
4451
4936
|
addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
|
|
4452
|
-
if ((0,
|
|
4453
|
-
const schemaFile = (0, import_node_path8.resolve)(project.root, (0,
|
|
4937
|
+
if ((0, import_core23.schemaInsideTree)(project.config) === void 0) {
|
|
4938
|
+
const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core23.schemaFileOf)(project.config));
|
|
4454
4939
|
addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
|
|
4455
4940
|
}
|
|
4456
4941
|
void validate();
|
|
@@ -4560,7 +5045,7 @@ var main = (0, import_citty18.defineCommand)({
|
|
|
4560
5045
|
}
|
|
4561
5046
|
});
|
|
4562
5047
|
function runMain() {
|
|
4563
|
-
(0,
|
|
5048
|
+
(0, import_core24.setKeychain)(defaultKeychain);
|
|
4564
5049
|
return (0, import_citty18.runMain)(main);
|
|
4565
5050
|
}
|
|
4566
5051
|
// Annotate the CommonJS export names for ESM import in node:
|