@penvhq/cli 0.14.0 → 0.16.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.js CHANGED
@@ -37,6 +37,7 @@ import { dirname, join as join2 } from "path";
37
37
  import {
38
38
  assertMigrated,
39
39
  candidatesFor,
40
+ environmentEntry as environmentEntry2,
40
41
  formatValueFile,
41
42
  isCanonicalSegment,
42
43
  isReservedToken,
@@ -55,24 +56,12 @@ import {
55
56
  import { FilesystemProvider } from "@penvhq/provider-filesystem";
56
57
 
57
58
  // src/env-flags.ts
58
- import { PenvError } from "@penvhq/core";
59
+ import { environmentNames, PenvError } from "@penvhq/core";
59
60
  var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
60
- var COMMAND_FLAGS = [
61
- ...BASE_RESERVED,
62
- "env",
63
- "out",
64
- "allow-decrypt",
65
- "destination",
66
- "dest",
67
- "d",
68
- "location",
69
- "l",
70
- "yes",
71
- "y"
72
- ];
61
+ var COMMAND_FLAGS = [...BASE_RESERVED, "env", "out", "allow-decrypt", "yes", "y"];
73
62
  function shadowedEnvironments(config) {
74
63
  const flags = new Set(COMMAND_FLAGS);
75
- return config.environments.filter((environment) => flags.has(environment));
64
+ return environmentNames(config).filter((environment) => flags.has(environment));
76
65
  }
77
66
  function shorthandCandidates(args, declared) {
78
67
  const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
@@ -85,13 +74,14 @@ function environmentFromShorthand(config, candidates, explicit) {
85
74
  if (candidates.length === 0) {
86
75
  return void 0;
87
76
  }
88
- const hits = candidates.filter((candidate) => config.environments.includes(candidate));
89
- const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
77
+ const declared = environmentNames(config);
78
+ const hits = candidates.filter((candidate) => declared.includes(candidate));
79
+ const strangers = candidates.filter((candidate) => !declared.includes(candidate));
90
80
  if (strangers.length > 0) {
91
81
  throw new PenvError(
92
82
  "UNKNOWN_FLAG",
93
83
  `${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
94
- `Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
84
+ `Declared environments work as bare flags: ${quoteList(declared)}. Anything else needs \`--env <name>\`.`
95
85
  );
96
86
  }
97
87
  if (hits.length > 1) {
@@ -118,6 +108,8 @@ import { createRequire } from "module";
118
108
  import { join, resolve } from "path";
119
109
  import { pathToFileURL } from "url";
120
110
  import {
111
+ environmentEntry,
112
+ environmentNames as environmentNames2,
121
113
  holdsProjection,
122
114
  LOCAL_EXTENSIONS_PATH,
123
115
  localExtensionsFile,
@@ -129,6 +121,7 @@ import {
129
121
  parseLocalExtensions,
130
122
  parseManifest,
131
123
  penvHome,
124
+ providerMissing,
132
125
  recordsDir
133
126
  } from "@penvhq/core";
134
127
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
@@ -195,14 +188,18 @@ async function loadPluginProvider(type, context) {
195
188
  function assertProvidersRegistered(config, projectRoot, options) {
196
189
  const local = localExtensions(projectRoot);
197
190
  const ci = options?.ci ?? isCi(process.env.CI);
198
- for (const [environment, provider] of Object.entries(config.providers)) {
199
- if (isProviderRegistered(provider.type)) {
191
+ for (const environment of environmentNames2(config)) {
192
+ const provider = environmentEntry(config, environment)?.provider;
193
+ if (typeof provider !== "string" || provider.trim().length === 0) {
194
+ throw providerMissing(environment);
195
+ }
196
+ if (isProviderRegistered(provider)) {
200
197
  continue;
201
198
  }
202
- if (ci && local.includes(provider.type)) {
203
- throw localExtensionInCi(provider.type, environment);
199
+ if (ci && local.includes(provider)) {
200
+ throw localExtensionInCi(provider, environment);
204
201
  }
205
- resolveExtension(provider.type, projectRoot, environment, local);
202
+ resolveExtension(provider, projectRoot, environment, local);
206
203
  }
207
204
  }
208
205
  function resolveExtension(type, projectRoot, environment, local = localExtensions(projectRoot)) {
@@ -360,11 +357,11 @@ function schemaShapeFileOf(project) {
360
357
  return selfContainedSchemaModule(project.root, schemaFile) ?? SCHEMA_SHAPE_FILE;
361
358
  }
362
359
  async function sourceProviderFor(project, environment) {
363
- const providerConfig = project.config.providers[environment];
360
+ const providerConfig = environmentEntry2(project.config, environment);
364
361
  if (providerConfig === void 0) {
365
362
  return createProvider(LOCAL_TREE_TYPE, { root: project.root, config: project.config });
366
363
  }
367
- return createSourceProvider(providerConfig.type, {
364
+ return createSourceProvider(providerConfig.provider, {
368
365
  root: project.root,
369
366
  config: project.config,
370
367
  providerConfig,
@@ -376,7 +373,7 @@ function targetEnvironment(project, explicit, shorthand) {
376
373
  return resolveEnvironment(project.config, explicit ?? fromFlags);
377
374
  }
378
375
  var KEY_SEPARATOR = /[./\\]/;
379
- var NO_ENVIRONMENTS = { environments: [], providers: {} };
376
+ var NO_ENVIRONMENTS = { environments: {} };
380
377
  function refFromKey(key, config) {
381
378
  const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
382
379
  const name = segments[segments.length - 1];
@@ -624,6 +621,8 @@ import { basename, dirname as dirname2, join as join3, resolve as resolve2 } fro
624
621
  import {
625
622
  ARTIFACT_BUILD_COMMAND,
626
623
  assertArtifactFor,
624
+ environmentEntry as environmentEntry3,
625
+ environmentNames as environmentNames4,
627
626
  isPublicVariable,
628
627
  isSecret,
629
628
  keySourceFrom,
@@ -648,7 +647,7 @@ import { defineCommand as defineCommand3 } from "citty";
648
647
 
649
648
  // src/dotenv-files.ts
650
649
  import { readdirSync } from "fs";
651
- import { isLegalEnvironmentName, RESERVED_TOKENS } from "@penvhq/core";
650
+ import { environmentNames as environmentNames3, isLegalEnvironmentName, RESERVED_TOKENS } from "@penvhq/core";
652
651
  var SHARED = ".env";
653
652
  var LOCAL = "local";
654
653
  var NOT_ENVIRONMENTS = [
@@ -721,8 +720,9 @@ function discoverDotenvFiles(root) {
721
720
  return files.sort((a, b) => order(a) < order(b) ? -1 : order(a) > order(b) ? 1 : 0);
722
721
  }
723
722
  function activeDotenvFiles(root, config) {
723
+ const declared = environmentNames3(config);
724
724
  return discoverDotenvFiles(root).filter(
725
- (file) => file.environment === void 0 || config.environments.includes(file.environment)
725
+ (file) => file.environment === void 0 || declared.includes(file.environment)
726
726
  );
727
727
  }
728
728
  function environmentsDeclaredBy(files) {
@@ -1480,11 +1480,12 @@ function packageManifest(specifier, root) {
1480
1480
  }
1481
1481
  function declaredCredentials(config, root) {
1482
1482
  const declared = {};
1483
- for (const provider of Object.values(config.providers)) {
1484
- if (own(declared, provider.type) !== void 0) {
1483
+ for (const environment of environmentNames4(config)) {
1484
+ const provider = environmentEntry3(config, environment)?.provider;
1485
+ if (provider === void 0 || own(declared, provider) !== void 0) {
1485
1486
  continue;
1486
1487
  }
1487
- const penv = packageManifest(provider.type, root)?.penv;
1488
+ const penv = packageManifest(provider, root)?.penv;
1488
1489
  const credentials = isPlainObject(penv) ? penv.credentials : void 0;
1489
1490
  if (credentials === void 0) {
1490
1491
  continue;
@@ -1492,11 +1493,11 @@ function declaredCredentials(config, root) {
1492
1493
  if (!Array.isArray(credentials) || credentials.some((name) => typeof name !== "string" || !VARIABLE.test(name))) {
1493
1494
  throw new PenvError5(
1494
1495
  "PROVIDER_CREDENTIALS_INVALID",
1495
- `\`${provider.type}\` declares \`penv.credentials\`, and it is not a list of variable names`,
1496
+ `\`${provider}\` declares \`penv.credentials\`, and it is not a list of variable names`,
1496
1497
  `Its package.json should read \`"penv": { "credentials": ["ACME_TOKEN"] }\` \u2014 penv strips exactly what an extension declares, so it will not guess at a declaration it cannot read.`
1497
1498
  );
1498
1499
  }
1499
- declared[provider.type] = credentials;
1500
+ declared[provider] = credentials;
1500
1501
  }
1501
1502
  return declared;
1502
1503
  }
@@ -2155,6 +2156,7 @@ import {
2155
2156
  accessPath as accessPath3,
2156
2157
  assertNever,
2157
2158
  effectiveMeta,
2159
+ environmentEntry as environmentEntry5,
2158
2160
  formatValueFile as formatValueFile3,
2159
2161
  holdsProjection as holdsProjection4,
2160
2162
  holdsRecords,
@@ -2174,6 +2176,7 @@ import { defineCommand as defineCommand7 } from "citty";
2174
2176
  // src/commands/push.ts
2175
2177
  import {
2176
2178
  checkNameCollisions as checkNameCollisions2,
2179
+ environmentEntry as environmentEntry4,
2177
2180
  holdsProjection as holdsProjection3,
2178
2181
  PenvError as PenvError8,
2179
2182
  recordPath,
@@ -2227,38 +2230,23 @@ function lineReader(input = process.stdin, output = process.stdout) {
2227
2230
  // src/commands/push.ts
2228
2231
  var LAST_PUSHED_KEY = "lastPushedAt";
2229
2232
  async function destinationFor(project, environment, options) {
2230
- if (options.destination !== void 0) {
2231
- const providerConfig = {
2232
- type: options.destination,
2233
- ...options.location === void 0 ? {} : { location: options.location }
2234
- };
2235
- const provider2 = options.provider ?? await createSourceProvider(options.destination, {
2236
- root: project.root,
2237
- config: project.config,
2238
- providerConfig,
2239
- environment
2240
- });
2241
- return { provider: provider2, location: options.location };
2242
- }
2243
- const declared = project.config.providers[environment];
2244
- const location = declared?.location;
2245
- if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
2233
+ const declared = environmentEntry4(project.config, environment);
2234
+ if (declared === void 0 || declared.provider === LOCAL_TREE_TYPE) {
2246
2235
  throw new PenvError8(
2247
2236
  "NO_DESTINATION",
2248
2237
  `Environment ${environment}'s provider is the local records tree itself, so penv has nowhere to push`,
2249
- `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>\`.`
2238
+ `Declare the backend that holds it in penv.config.ts \u2014 e.g. \`${environment}: { provider: "@penvhq/provider-github", repository: "owner/repo" }\` \u2014 and run this again. A push only ever goes where the config says this environment lives.`
2250
2239
  );
2251
2240
  }
2252
2241
  if (options.provider !== void 0) {
2253
- return { provider: options.provider, location };
2242
+ return options.provider;
2254
2243
  }
2255
- const provider = await createSourceProvider(declared.type, {
2244
+ return createSourceProvider(declared.provider, {
2256
2245
  root: project.root,
2257
2246
  config: project.config,
2258
2247
  providerConfig: declared,
2259
2248
  environment
2260
2249
  });
2261
- return { provider, location };
2262
2250
  }
2263
2251
  function plan(resolutions, config, environment, allowDecrypt) {
2264
2252
  const outbound = [];
@@ -2342,7 +2330,7 @@ async function ensureTargetApproved(provider, environment, options) {
2342
2330
  await provider.ensureTarget(environment);
2343
2331
  return true;
2344
2332
  }
2345
- async function pushProjection(project, provider, environment, location, options) {
2333
+ async function pushProjection(project, provider, environment, options) {
2346
2334
  const tree = localTree(project);
2347
2335
  const keys = keySourceFor(project, environment);
2348
2336
  const resolutions = resolveAllSync(tree, environment, keys, true);
@@ -2373,7 +2361,6 @@ async function pushProjection(project, provider, environment, location, options)
2373
2361
  environment,
2374
2362
  destination: provider.type,
2375
2363
  mode: "projection",
2376
- location,
2377
2364
  pushed: outbound.length,
2378
2365
  meta: 0,
2379
2366
  repositorySecrets,
@@ -2389,7 +2376,7 @@ function crossesToRecords(file, environment) {
2389
2376
  }
2390
2377
  return scope.kind === "environment" && scope.environment === environment;
2391
2378
  }
2392
- async function pushRecords(project, provider, environment, location) {
2379
+ async function pushRecords(project, provider, environment) {
2393
2380
  const tree = localTree(project);
2394
2381
  const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
2395
2382
  let pushed = 0;
@@ -2415,7 +2402,6 @@ async function pushRecords(project, provider, environment, location) {
2415
2402
  environment,
2416
2403
  destination: provider.type,
2417
2404
  mode: "records",
2418
- location,
2419
2405
  pushed,
2420
2406
  meta,
2421
2407
  repositorySecrets: 0,
@@ -2427,11 +2413,11 @@ async function pushRecords(project, provider, environment, location) {
2427
2413
  async function runPush(options) {
2428
2414
  const project = openProject(options.cwd);
2429
2415
  const environment = targetEnvironment(project, options.environment, options.envFlags);
2430
- const { provider, location } = await destinationFor(project, environment, options);
2416
+ const provider = await destinationFor(project, environment, options);
2431
2417
  if (holdsProjection3(provider)) {
2432
- return pushProjection(project, provider, environment, location, options);
2418
+ return pushProjection(project, provider, environment, options);
2433
2419
  }
2434
- return pushRecords(project, provider, environment, location);
2420
+ return pushRecords(project, provider, environment);
2435
2421
  }
2436
2422
  function renderPush(result2) {
2437
2423
  if (result2.pushed === 0 && result2.meta === 0) {
@@ -2443,7 +2429,7 @@ function renderPush(result2) {
2443
2429
  }
2444
2430
  ]);
2445
2431
  }
2446
- const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
2432
+ const target = `${result2.destination} for environment ${result2.environment}`;
2447
2433
  if (result2.mode === "records") {
2448
2434
  return formatRows([
2449
2435
  {
@@ -2492,6 +2478,19 @@ function renderPush(result2) {
2492
2478
  }
2493
2479
  return formatRows(rows);
2494
2480
  }
2481
+ var REDIRECT_FLAGS = ["destination", "dest", "d", "location", "l"];
2482
+ function assertNoRedirect(args) {
2483
+ const given = REDIRECT_FLAGS.filter((flag) => args[flag] !== void 0);
2484
+ const named = given[0];
2485
+ if (named === void 0) {
2486
+ return;
2487
+ }
2488
+ throw new PenvError8(
2489
+ "REMOVED_FLAG",
2490
+ `\`--${named}\` is not a flag \`penv push\` takes`,
2491
+ "A push only ever goes where the config says this environment lives. Declare the destination in `environments.<env>` \u2014 the provider package, addressed in that provider's own words \u2014 and push with `--env <name>`."
2492
+ );
2493
+ }
2495
2494
  var pushCommand = defineCommand6({
2496
2495
  meta: {
2497
2496
  name: "push",
@@ -2503,16 +2502,6 @@ var pushCommand = defineCommand6({
2503
2502
  type: "boolean",
2504
2503
  description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
2505
2504
  },
2506
- destination: {
2507
- type: "string",
2508
- alias: ["dest", "d"],
2509
- description: "Push once to this provider package instead of the declared one"
2510
- },
2511
- location: {
2512
- type: "string",
2513
- alias: "l",
2514
- description: "The destination-side place, when --destination needs one"
2515
- },
2516
2505
  yes: {
2517
2506
  type: "boolean",
2518
2507
  alias: "y",
@@ -2521,25 +2510,13 @@ var pushCommand = defineCommand6({
2521
2510
  },
2522
2511
  run({ args }) {
2523
2512
  return guard(async () => {
2513
+ assertNoRedirect(args);
2524
2514
  const result2 = await runPush({
2525
2515
  cwd: process.cwd(),
2526
2516
  ...args.env === void 0 ? {} : { environment: args.env },
2527
2517
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
2528
- ...args.destination === void 0 ? {} : { destination: args.destination },
2529
- ...args.location === void 0 ? {} : { location: args.location },
2530
2518
  ...args.yes === void 0 ? {} : { yes: args.yes },
2531
- envFlags: shorthandCandidates(args, [
2532
- "env",
2533
- "allow-decrypt",
2534
- "allowDecrypt",
2535
- "destination",
2536
- "dest",
2537
- "d",
2538
- "location",
2539
- "l",
2540
- "yes",
2541
- "y"
2542
- ])
2519
+ envFlags: shorthandCandidates(args, ["env", "allow-decrypt", "allowDecrypt", "yes", "y"])
2543
2520
  });
2544
2521
  write(renderPush(result2));
2545
2522
  });
@@ -2638,7 +2615,7 @@ async function runDoctor(options) {
2638
2615
  check: "provider",
2639
2616
  severity: "pass",
2640
2617
  label: "Provider",
2641
- subject: project.config.providers[environment]?.type ?? project.provider.type
2618
+ subject: environmentEntry5(project.config, environment)?.provider ?? project.provider.type
2642
2619
  });
2643
2620
  return {
2644
2621
  environment,
@@ -2908,8 +2885,8 @@ async function projectionFindings(project, environment, override) {
2908
2885
  if (override !== void 0) {
2909
2886
  projection = override;
2910
2887
  } else {
2911
- const declared = project.config.providers[environment];
2912
- if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
2888
+ const declared = environmentEntry5(project.config, environment);
2889
+ if (declared === void 0 || declared.provider === LOCAL_TREE_TYPE) {
2913
2890
  return [];
2914
2891
  }
2915
2892
  let source;
@@ -2921,7 +2898,7 @@ async function projectionFindings(project, environment, override) {
2921
2898
  check: "projection-unreachable",
2922
2899
  severity: "unknown",
2923
2900
  label: "Destination",
2924
- subject: `could not build the ${declared.type} provider for ${environment}`,
2901
+ subject: `could not build the ${declared.provider} provider for ${environment}`,
2925
2902
  detail: errorDetail(error)
2926
2903
  }
2927
2904
  ];
@@ -3194,8 +3171,8 @@ function refPathOf(ref) {
3194
3171
  return [...ref.namespace, ref.name].join("/");
3195
3172
  }
3196
3173
  async function rotationSubjects(project, environment, local, override) {
3197
- const providerConfig = project.config.providers[environment];
3198
- if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
3174
+ const providerConfig = environmentEntry5(project.config, environment);
3175
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.provider === LOCAL_TREE_TYPE)) {
3199
3176
  return { kind: "read", subjects: local };
3200
3177
  }
3201
3178
  const source = override ?? await sourceProviderFor(project, environment);
@@ -3217,7 +3194,7 @@ async function rotationSubjects(project, environment, local, override) {
3217
3194
  check: "rotation-overdue",
3218
3195
  severity: "unknown",
3219
3196
  label: "Rotation",
3220
- subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
3197
+ subject: `could not reach the ${providerConfig?.provider ?? source.type} source of truth for ${environment}`,
3221
3198
  detail: errorDetail(error)
3222
3199
  }
3223
3200
  };
@@ -3338,8 +3315,8 @@ async function readRelevant(provider, environment) {
3338
3315
  return entries;
3339
3316
  }
3340
3317
  async function providerDriftFindings(project, environment, override) {
3341
- const providerConfig = project.config.providers[environment];
3342
- if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
3318
+ const providerConfig = environmentEntry5(project.config, environment);
3319
+ if (providerConfig === void 0 || providerConfig.provider === LOCAL_TREE_TYPE) {
3343
3320
  return [
3344
3321
  {
3345
3322
  check: "provider-value-drift",
@@ -3364,7 +3341,7 @@ async function providerDriftFindings(project, environment, override) {
3364
3341
  check: "provider-value-drift",
3365
3342
  severity: "unknown",
3366
3343
  label: "Provider values",
3367
- subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
3344
+ subject: `could not reach the ${providerConfig.provider} provider for ${environment}`,
3368
3345
  detail: errorDetail(error)
3369
3346
  }
3370
3347
  ];
@@ -3383,7 +3360,7 @@ async function providerDriftFindings(project, environment, override) {
3383
3360
  severity: "unknown",
3384
3361
  label: "Provider value unreadable",
3385
3362
  subject: formatValueFile3(here.file),
3386
- detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
3363
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.provider} source of truth`
3387
3364
  });
3388
3365
  continue;
3389
3366
  }
@@ -3393,7 +3370,7 @@ async function providerDriftFindings(project, environment, override) {
3393
3370
  severity: "failure",
3394
3371
  label: "Provider value drift",
3395
3372
  subject: formatValueFile3(here.file),
3396
- detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
3373
+ detail: `the local tree and the ${providerConfig.provider} source of truth hold different values`
3397
3374
  });
3398
3375
  }
3399
3376
  continue;
@@ -3407,7 +3384,7 @@ async function providerDriftFindings(project, environment, override) {
3407
3384
  severity: "warning",
3408
3385
  label: here !== void 0 ? "Only in the local tree" : "Only in the source",
3409
3386
  subject: formatValueFile3(present.file),
3410
- 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`
3387
+ detail: here !== void 0 ? `present locally, absent from the ${providerConfig.provider} source of truth` : `present in the ${providerConfig.provider} source of truth, absent from the local tree`
3411
3388
  });
3412
3389
  }
3413
3390
  if (findings.length > 0) {
@@ -3418,7 +3395,7 @@ async function providerDriftFindings(project, environment, override) {
3418
3395
  check: "provider-value-drift",
3419
3396
  severity: "pass",
3420
3397
  label: "Provider values",
3421
- subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
3398
+ subject: `every value matches the ${providerConfig.provider} source of truth for ${environment}`
3422
3399
  }
3423
3400
  ];
3424
3401
  }
@@ -3506,6 +3483,7 @@ import { defineCommand as defineCommand9 } from "citty";
3506
3483
 
3507
3484
  // src/commands/set.ts
3508
3485
  import {
3486
+ environmentNames as environmentNames5,
3509
3487
  formatValueFile as formatValueFile4,
3510
3488
  isSecret as isSecret4,
3511
3489
  PenvError as PenvError9,
@@ -3535,7 +3513,7 @@ function targetScope(project, options, key) {
3535
3513
  throw new PenvError9(
3536
3514
  "ENVIRONMENT_FLAG_EMPTY",
3537
3515
  `\`--env\` for parameter ${key} names no environment`,
3538
- `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.`
3516
+ `Pass a declared environment \u2014 ${environmentNames5(project.config).map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
3539
3517
  );
3540
3518
  }
3541
3519
  return scopeFrom({ ...options, environment: targetEnvironment(project, environment) });
@@ -3548,7 +3526,7 @@ function sealFor(project, file, value, parameter, environment) {
3548
3526
  throw new PenvError9(
3549
3527
  "SECRET_SCOPE_AMBIGUOUS",
3550
3528
  `Parameter ${parameter} is a secret, and ${recordPath2(formatValueFile4(file))} names no environment`,
3551
- "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."
3529
+ "A key is declared by its environment's `keySource` in 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."
3552
3530
  );
3553
3531
  }
3554
3532
  return sealValue(file, value, keySourceFor(project, environment), parameter, environment);
@@ -4127,6 +4105,7 @@ import { copyFileSync, existsSync as existsSync7, readFileSync as readFileSync9
4127
4105
  import { basename as basename2, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve7 } from "path";
4128
4106
  import {
4129
4107
  assertNever as assertNever2,
4108
+ environmentNames as environmentNames7,
4130
4109
  FilenameGrammarError,
4131
4110
  findConfigFile as findConfigFile2,
4132
4111
  loadConfigFrom as loadConfigFrom2,
@@ -4383,6 +4362,7 @@ import { createInterface as createInterface2 } from "readline/promises";
4383
4362
  import {
4384
4363
  CUTOVER_PATH as CUTOVER_PATH3,
4385
4364
  DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
4365
+ environmentNames as environmentNames6,
4386
4366
  isLegalEnvironmentName as isLegalEnvironmentName2,
4387
4367
  loadConfigFrom,
4388
4368
  PENV_DIR as PENV_DIR2,
@@ -4687,8 +4667,14 @@ function environmentsFromFlag(flag) {
4687
4667
  }
4688
4668
  return [...new Set(names)];
4689
4669
  }
4670
+ function environmentsRecord(names) {
4671
+ return Object.fromEntries(names.map((environment) => [environment, LOCAL_PROVIDER]));
4672
+ }
4690
4673
  function configOf(decisions) {
4691
- return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
4674
+ return {
4675
+ environments: environmentsRecord(decisions.environments),
4676
+ schemaFile: decisions.schemaFile
4677
+ };
4692
4678
  }
4693
4679
  function declaredIn(root) {
4694
4680
  const file = join8(root, CONFIG_FILE);
@@ -4720,7 +4706,7 @@ function planInit(root, flags = {}) {
4720
4706
  if (schemaFile.length === 0) {
4721
4707
  throw emptyFlag("schema");
4722
4708
  }
4723
- const error = validateSchemaFile({ environments: [], providers: {}, schemaFile })[0];
4709
+ const error = validateSchemaFile({ environments: {}, schemaFile })[0];
4724
4710
  if (error !== void 0) {
4725
4711
  throw error;
4726
4712
  }
@@ -4743,7 +4729,7 @@ function planInit(root, flags = {}) {
4743
4729
  }
4744
4730
  const suggestedEnvironments = suggestEnvironments(root);
4745
4731
  const safeDefault = flags.yes === true && declared === void 0;
4746
- const environments = flags.environments ?? declared?.environments ?? (safeDefault ? [DEVELOPMENT] : []);
4732
+ const environments = flags.environments ?? (declared === void 0 ? void 0 : environmentNames6(declared)) ?? (safeDefault ? [DEVELOPMENT] : []);
4747
4733
  const defaultEnvironment = declared?.defaultEnvironment ?? (safeDefault ? DEVELOPMENT : void 0);
4748
4734
  if (safeDefault) {
4749
4735
  notes.push(
@@ -4876,27 +4862,27 @@ export { schema };
4876
4862
 
4877
4863
  ` + loadComment + loadCall;
4878
4864
  }
4865
+ function entryKey(name) {
4866
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
4867
+ }
4879
4868
  function renderEnvironments(decisions) {
4880
- const shared = " // Environments are a whitelist. A filename segment is an environment only if\n // it is declared here \u2014 penv never infers one from a folder or a filename.\n";
4869
+ const shared = " // Environments are a whitelist, and each entry is that environment's whole\n // declaration: which provider holds it, in that provider's own words, and\n // which key seals it. A segment is an environment only if it is named here.\n";
4881
4870
  if (decisions.environments.length === 0) {
4882
4871
  return `${shared} // It starts empty because which environments you deploy is not something penv
4883
4872
  // can read off your codebase, and an environment you do not have is worse
4884
- // than one you have not declared yet. Name yours, and give each a provider:
4885
- // environments: ["development", "production"],
4886
- // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
4887
- environments: [],
4888
-
4889
- providers: {},
4873
+ // than one you have not declared yet. Name yours:
4874
+ // environments: {
4875
+ // development: "@penvhq/provider-filesystem",
4876
+ // production: { provider: "@penvhq/provider-vault", path: "penv", keySource: "env" },
4877
+ // },
4878
+ environments: {},
4890
4879
  `;
4891
4880
  }
4892
- const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
4893
- const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
4881
+ const entries = decisions.environments.map((name) => ` ${entryKey(name)}: ${JSON.stringify(LOCAL_PROVIDER)},
4894
4882
  `).join("");
4895
- return `${shared} environments: [${names}],
4896
-
4897
- // One entry per environment: where that environment's values are read from.
4898
- providers: {
4899
- ${providers} },
4883
+ return `${shared} // A provider that needs no fields is just its package name.
4884
+ environments: {
4885
+ ${entries} },
4900
4886
  `;
4901
4887
  }
4902
4888
  function renderConfigModule(decisions) {
@@ -5415,13 +5401,13 @@ function planCutover(input) {
5415
5401
  const declared = declaredIn(root);
5416
5402
  const named = environmentsDeclaredBy(selected);
5417
5403
  const chosen = named.length > 0 ? named : [requireEnvironment(input)];
5418
- const environments = declared === void 0 ? chosen : declared.environments;
5404
+ const environments = declared === void 0 ? chosen : environmentNames6(declared);
5419
5405
  for (const environment of chosen) {
5420
5406
  if (!environments.includes(environment)) {
5421
5407
  throw new PenvError15(
5422
5408
  "INIT_ENVIRONMENT_UNDECLARED",
5423
5409
  `${CONFIG_FILE} declares ${describeList(environments)}, and adopting these files needs \`${environment}\` declared too`,
5424
- `Add \`${environment}\` to \`environments\` in ${CONFIG_FILE}, with a provider for it, then run \`penv init\` again. Nothing was changed.`
5410
+ `Add \`${environment}: "${LOCAL_PROVIDER}"\` to \`environments\` in ${CONFIG_FILE}, or name the backend that holds it, then run \`penv init\` again. Nothing was changed.`
5425
5411
  );
5426
5412
  }
5427
5413
  }
@@ -5488,10 +5474,7 @@ function distinct(refs) {
5488
5474
  }
5489
5475
  function configFor(decisions) {
5490
5476
  return {
5491
- environments: decisions.environments,
5492
- providers: Object.fromEntries(
5493
- decisions.environments.map((environment) => [environment, { type: LOCAL_PROVIDER }])
5494
- ),
5477
+ environments: environmentsRecord(decisions.environments),
5495
5478
  schemaFile: decisions.schemaFile,
5496
5479
  ...decisions.defaultEnvironment === void 0 ? {} : { defaultEnvironment: decisions.defaultEnvironment },
5497
5480
  ...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: [...decisions.publicPrefixes] }
@@ -5831,7 +5814,8 @@ function offeredEnvironment(root) {
5831
5814
  if (declared === void 0) {
5832
5815
  return DEVELOPMENT;
5833
5816
  }
5834
- return declared.defaultEnvironment ?? (declared.environments.includes(DEVELOPMENT) ? DEVELOPMENT : declared.environments[0] ?? DEVELOPMENT);
5817
+ const names = environmentNames6(declared);
5818
+ return declared.defaultEnvironment ?? (names.includes(DEVELOPMENT) ? DEVELOPMENT : names[0] ?? DEVELOPMENT);
5835
5819
  }
5836
5820
  async function cutoverWithYes(root, base, adoption) {
5837
5821
  const selected = selectionForYes(adoption);
@@ -5939,10 +5923,11 @@ var BACKUP_SUFFIX = ".backup";
5939
5923
  var DOTENV_SEGMENT = "env";
5940
5924
  var LOCAL2 = "local";
5941
5925
  function assertDeclared(segment, config) {
5942
- if (config.environments.includes(segment)) {
5926
+ const declared = environmentNames7(config);
5927
+ if (declared.includes(segment)) {
5943
5928
  return segment;
5944
5929
  }
5945
- throw new UnknownEnvironmentError(segment, config.environments);
5930
+ throw new UnknownEnvironmentError(segment, declared);
5946
5931
  }
5947
5932
  function scopeFromFilename(file, config) {
5948
5933
  const name = basename2(file);
@@ -6012,7 +5997,7 @@ function explicitEnvironment(options, source, config) {
6012
5997
  throw new PenvError16(
6013
5998
  "IMPORT_ENV_FLAG_EMPTY",
6014
5999
  `\`--env\` for the import of ${source} names no environment`,
6015
- `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.`
6000
+ `Pass a declared environment \u2014 ${environmentNames7(config).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.`
6016
6001
  );
6017
6002
  }
6018
6003
  function assertEnvironmentAgrees(derived, explicit, source) {
@@ -6039,7 +6024,7 @@ function environmentNamed(file, explicit) {
6039
6024
  }
6040
6025
  function decisionsOf(config, cwd) {
6041
6026
  return {
6042
- environments: config.environments,
6027
+ environments: environmentNames7(config),
6043
6028
  // `import` re-scaffolds env.ts for an existing project; injection is an init
6044
6029
  // choice, so it is not turned on here.
6045
6030
  inject: false,
@@ -6095,7 +6080,7 @@ function importDotenv(options) {
6095
6080
  backup,
6096
6081
  scope,
6097
6082
  environment,
6098
- environments: config.environments,
6083
+ environments: environmentNames7(config),
6099
6084
  variables: parsed.entries.length,
6100
6085
  orphanComments: parsed.orphanComments,
6101
6086
  steps
@@ -6190,7 +6175,7 @@ var importCommand = defineCommand14({
6190
6175
 
6191
6176
  // src/commands/key.ts
6192
6177
  import { randomBytes } from "crypto";
6193
- import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError18 } from "@penvhq/core";
6178
+ import { KEY_BYTES, KEYCHAIN_SERVICE, keyConfigFor, PenvError as PenvError18 } from "@penvhq/core";
6194
6179
  import { defineCommand as defineCommand15 } from "citty";
6195
6180
 
6196
6181
  // src/keychain.ts
@@ -6242,12 +6227,12 @@ function envVarFor(id) {
6242
6227
  function runKeyCreate(options) {
6243
6228
  const project = openProject(options.cwd);
6244
6229
  const environment = targetEnvironment(project, options.environment);
6245
- const declared = project.config.keys?.[environment];
6230
+ const declared = keyConfigFor(project.config, environment);
6246
6231
  if (declared === void 0) {
6247
6232
  throw new PenvError18(
6248
6233
  "KEY_SOURCE_UNDECLARED",
6249
6234
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
6250
- `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
6235
+ `Give its entry in penv.config.ts a \`keySource\` \u2014 e.g. \`${environment}: { provider: "@penvhq/provider-filesystem", keySource: "env" }\` \u2014 then run this again. The key is named after the environment unless the object form names another.`
6251
6236
  );
6252
6237
  }
6253
6238
  const key = randomBytes(KEY_BYTES).toString("base64");
@@ -6616,7 +6601,7 @@ async function planFile(project, source, target, parameter) {
6616
6601
  throw new PenvError20(
6617
6602
  "SECRET_SCOPE_AMBIGUOUS",
6618
6603
  `${recordPath7(formatValueFile6(source))} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
6619
- "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."
6604
+ "A key is declared by its environment's `keySource` in penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
6620
6605
  );
6621
6606
  }
6622
6607
  const keys = keySourceFor(project, environment);