@penvhq/cli 0.2.0 → 0.3.1

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
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { setKeychain } from "@penvhq/core";
3
- import { runMain as cittyRunMain, defineCommand as defineCommand17 } from "citty";
3
+ import { runMain as cittyRunMain, defineCommand as defineCommand18 } from "citty";
4
4
 
5
5
  // src/commands/doctor.ts
6
6
  import {
@@ -26,12 +26,14 @@ import { dirname as dirname2, resolve as resolve2 } from "path";
26
26
  import {
27
27
  candidatesFor,
28
28
  formatValueFile,
29
+ isCanonicalSegment,
29
30
  isReservedToken,
30
31
  loadConfig,
31
32
  openValue,
32
33
  PenvError as PenvError2,
33
34
  parameterId,
34
35
  ReservedTokenError,
36
+ refFromAccessPath,
35
37
  resolveEnvironment,
36
38
  resolveKeySource
37
39
  } from "@penvhq/core";
@@ -43,13 +45,20 @@ import { dirname, resolve } from "path";
43
45
  import { pathToFileURL } from "url";
44
46
  import { PenvError } from "@penvhq/core";
45
47
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
48
+ import { createKubernetesProvider } from "@penvhq/provider-kubernetes";
46
49
  import { createMockProvider } from "@penvhq/provider-mock";
50
+ import { createSsmProvider } from "@penvhq/provider-ssm";
47
51
  import { createVaultProvider } from "@penvhq/provider-vault";
48
52
  var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
49
53
  var LOCAL_TREE_TYPE = "filesystem";
50
54
  var REGISTRY = /* @__PURE__ */ new Map([
51
55
  [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
52
56
  ["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
57
+ ["ssm", ({ providerConfig }) => createSsmProvider({ path: providerConfig?.path ?? "penv" })],
58
+ [
59
+ "kubernetes",
60
+ ({ providerConfig }) => createKubernetesProvider(kubernetesOptions(providerConfig))
61
+ ],
53
62
  ["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
54
63
  ]);
55
64
  var CONTRACT_METHODS = [
@@ -152,6 +161,14 @@ function assertSatisfiesContract(provider, specifier) {
152
161
  }
153
162
  }
154
163
  }
164
+ function kubernetesOptions(providerConfig) {
165
+ const path = providerConfig?.path ?? "penv";
166
+ const slash = path.indexOf("/");
167
+ if (slash === -1) return { secretName: path };
168
+ const namespace = path.slice(0, slash);
169
+ const secretName = path.slice(slash + 1) || "penv";
170
+ return namespace === "" ? { secretName } : { namespace, secretName };
171
+ }
155
172
  function unknownProvider(type, environment, specifier) {
156
173
  const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
157
174
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
@@ -220,6 +237,26 @@ function refFromKey(key, config) {
220
237
  }
221
238
  return { namespace: segments.slice(0, -1), name };
222
239
  }
240
+ function assertWritableKey(key) {
241
+ const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
242
+ if (segments.length === 0 || segments.every((s) => isCanonicalSegment(s))) {
243
+ return;
244
+ }
245
+ const ref = refFromAccessPath(segments);
246
+ if (ref !== void 0) {
247
+ const suggestion = [...ref.namespace, ref.name].join("/");
248
+ throw new PenvError2(
249
+ "PARAMETER_KEY_CASING",
250
+ `\`${key}\` is not a canonical parameter name`,
251
+ `Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
252
+ );
253
+ }
254
+ throw new PenvError2(
255
+ "PARAMETER_KEY_UNREACHABLE",
256
+ `No value file can be named that reaches \`${key}\``,
257
+ "Parameter files are lower-case and hyphenated, and this key maps to no such file \u2014 a run of capitals like `apiURL` cannot be reached (use `api-url`, which the schema reads as `apiUrl`). Run `penv validate` or `penv fill` to see the names penv expects."
258
+ );
259
+ }
223
260
  function keySourceFor(project, environment) {
224
261
  return resolveKeySource(project.config, environment);
225
262
  }
@@ -266,7 +303,7 @@ import {
266
303
  accessPath,
267
304
  isReservedToken as isReservedToken2,
268
305
  parameterId as parameterId2,
269
- refFromAccessPath,
306
+ refFromAccessPath as refFromAccessPath2,
270
307
  variableName
271
308
  } from "@penvhq/core";
272
309
  function defOf(node) {
@@ -374,7 +411,7 @@ function computeDrift(input) {
374
411
  continue;
375
412
  }
376
413
  const path = leaf.path.join(".");
377
- const ref = refFromAccessPath(leaf.path);
414
+ const ref = refFromAccessPath2(leaf.path);
378
415
  if (ref === void 0 || isReservedToken2(ref.name, config)) {
379
416
  declared.push({
380
417
  subject: path,
@@ -646,6 +683,7 @@ import { pathToFileURL as pathToFileURL2 } from "url";
646
683
  import {
647
684
  accessPath as accessPath2,
648
685
  checkNameCollisions as checkNameCollisions2,
686
+ jitiFor,
649
687
  NameCollisionError,
650
688
  PenvError as PenvError5,
651
689
  ReservedTokenError as ReservedTokenError2,
@@ -654,7 +692,6 @@ import {
654
692
  validateConfig
655
693
  } from "@penvhq/core";
656
694
  import { defineCommand as defineCommand2 } from "citty";
657
- import { createJiti } from "jiti";
658
695
  var SCHEMA_EXPORT = "schema";
659
696
  var LABELS = {
660
697
  config: "Config",
@@ -733,7 +770,7 @@ function loadSchema(project, environment) {
733
770
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
734
771
  }
735
772
  async function loadSchemaExclusively(file, schemaPath, environment) {
736
- const jiti = createJiti(file, { interopDefault: false, moduleCache: false });
773
+ const jiti = jitiFor(file);
737
774
  const previous = process.env.PENV_ENV;
738
775
  process.env.PENV_ENV = environment;
739
776
  let loaded;
@@ -1735,6 +1772,7 @@ async function sealAwareWrite(options) {
1735
1772
  }
1736
1773
  async function runSet(options) {
1737
1774
  const project = openProject(options.cwd);
1775
+ assertWritableKey(options.key);
1738
1776
  const ref = refFromKey(options.key, project.config);
1739
1777
  const scope = targetScope(project, options, options.key);
1740
1778
  const environment = policyEnvironment(project, options);
@@ -1937,18 +1975,123 @@ var decryptCommand = defineCommand5({
1937
1975
  }
1938
1976
  });
1939
1977
 
1978
+ // src/commands/fill.ts
1979
+ import { createInterface } from "readline/promises";
1980
+ import { PenvError as PenvError8 } from "@penvhq/core";
1981
+ import { defineCommand as defineCommand6 } from "citty";
1982
+ var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1983
+ async function runFill(options) {
1984
+ const validation = await runValidate({
1985
+ cwd: options.cwd,
1986
+ ...options.environment === void 0 ? {} : { environment: options.environment }
1987
+ });
1988
+ const environment = validation.environment;
1989
+ const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
1990
+ if (blockers.length > 0) {
1991
+ const detail = blockers.map(
1992
+ (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
1993
+ ).join("\n");
1994
+ throw new PenvError8(
1995
+ "FILL_BLOCKED",
1996
+ `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
1997
+ ${detail}`,
1998
+ `Fix these \u2014 \`penv validate\` reports them \u2014 then run \`penv fill\`. If you have not written a schema yet, declare the required parameters in ${PENV_DIR}/env.ts.`
1999
+ );
2000
+ }
2001
+ const written = [];
2002
+ const skipped2 = [];
2003
+ const unreachable = [];
2004
+ for (const drift of validation.drift.declared) {
2005
+ if (drift.ref === void 0) {
2006
+ unreachable.push({ subject: drift.subject, remedy: drift.remedy });
2007
+ continue;
2008
+ }
2009
+ const ref = drift.ref;
2010
+ const key = [...ref.namespace, ref.name].join("/");
2011
+ const value = await options.ask({ parameter: key, environment, secret: false });
2012
+ if (value === void 0 || value === "") {
2013
+ skipped2.push(drift.subject);
2014
+ continue;
2015
+ }
2016
+ const result2 = await runSet({ cwd: options.cwd, key, value, environment });
2017
+ written.push({
2018
+ parameter: drift.subject,
2019
+ location: result2.location,
2020
+ encrypted: result2.encrypted
2021
+ });
2022
+ }
2023
+ return { environment, written, skipped: skipped2, unreachable };
2024
+ }
2025
+ function summaryLine(result2) {
2026
+ const filled = result2.written.length;
2027
+ if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
2028
+ return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2029
+ }
2030
+ const parts = [`${filled} written`];
2031
+ if (result2.skipped.length > 0) {
2032
+ parts.push(`${result2.skipped.length} skipped`);
2033
+ }
2034
+ if (result2.unreachable.length > 0) {
2035
+ parts.push(`${result2.unreachable.length} unreachable`);
2036
+ }
2037
+ return `${parts.join(", ")} for environment ${result2.environment}`;
2038
+ }
2039
+ function renderFill(result2) {
2040
+ const rows = result2.written.map((entry) => ({
2041
+ glyph: CHECK,
2042
+ label: "Wrote",
2043
+ subject: `${PENV_DIR}/${entry.location}`,
2044
+ // Meta decided the seal, not the answer, so the line says so — as `set` does.
2045
+ ...entry.encrypted ? { detail: "encrypted, per the parameter's meta policy" } : {}
2046
+ }));
2047
+ for (const entry of result2.unreachable) {
2048
+ rows.push({ glyph: WARN, label: "Unreachable", subject: entry.subject, detail: entry.remedy });
2049
+ }
2050
+ const lines = formatRows(rows);
2051
+ lines.push(summaryLine(result2));
2052
+ return lines;
2053
+ }
2054
+ var fillCommand = defineCommand6({
2055
+ meta: {
2056
+ name: "fill",
2057
+ description: "Prompt for each declared parameter the tree has no value for"
2058
+ },
2059
+ args: {
2060
+ env: { type: "string", description: "The environment to fill" }
2061
+ },
2062
+ run({ args }) {
2063
+ return guard(async () => {
2064
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2065
+ const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2066
+ try {
2067
+ write(
2068
+ renderFill(
2069
+ await runFill({
2070
+ cwd: process.cwd(),
2071
+ ...args.env === void 0 ? {} : { environment: args.env },
2072
+ ask
2073
+ })
2074
+ )
2075
+ );
2076
+ } finally {
2077
+ rl.close();
2078
+ }
2079
+ });
2080
+ }
2081
+ });
2082
+
1940
2083
  // src/commands/generate.ts
1941
2084
  import { writeFileSync } from "fs";
1942
2085
  import { isAbsolute, relative, resolve as resolve3 } from "path";
1943
2086
  import {
1944
2087
  checkNameCollisions as checkNameCollisions3,
1945
2088
  effectiveMeta as effectiveMeta2,
1946
- PenvError as PenvError8,
2089
+ PenvError as PenvError9,
1947
2090
  requireValue as requireValue2,
1948
2091
  serializeDotenv,
1949
2092
  variableName as variableName4
1950
2093
  } from "@penvhq/core";
1951
- import { defineCommand as defineCommand6 } from "citty";
2094
+ import { defineCommand as defineCommand7 } from "citty";
1952
2095
  var DEFAULT_OUTPUT = ".env";
1953
2096
  function entriesFor(project, environment, allowDecrypt) {
1954
2097
  const keys = keySourceFor(project, environment);
@@ -1967,7 +2110,7 @@ function entriesFor(project, environment, allowDecrypt) {
1967
2110
  const winner = resolution.winner;
1968
2111
  if (winner?.file.encrypted === true) {
1969
2112
  if (!allowDecrypt) {
1970
- throw new PenvError8(
2113
+ throw new PenvError9(
1971
2114
  "ENCRYPTED_VALUE_REFUSED",
1972
2115
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
1973
2116
  `Re-run with \`--allow-decrypt\` to write the decrypted value into the artifact, or generate for an environment whose values are plaintext. The artifact is gitignored; a committed plaintext secret is a \`penv doctor\` failure.`
@@ -2025,7 +2168,7 @@ function renderGenerate(result2, cwd) {
2025
2168
  }
2026
2169
  return formatRows(rows);
2027
2170
  }
2028
- var generateCommand = defineCommand6({
2171
+ var generateCommand = defineCommand7({
2029
2172
  meta: { name: "generate", description: "Write a standard .env artifact for deploy targets" },
2030
2173
  args: {
2031
2174
  env: { type: "string", description: "The environment to generate for" },
@@ -2050,8 +2193,8 @@ var generateCommand = defineCommand6({
2050
2193
  });
2051
2194
 
2052
2195
  // src/commands/get.ts
2053
- import { PenvError as PenvError9, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
2054
- import { defineCommand as defineCommand7 } from "citty";
2196
+ import { PenvError as PenvError10, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
2197
+ import { defineCommand as defineCommand8 } from "citty";
2055
2198
  async function runGet(options) {
2056
2199
  const project = openProject(options.cwd);
2057
2200
  const environment = targetEnvironment(project, options.environment);
@@ -2060,7 +2203,7 @@ async function runGet(options) {
2060
2203
  const resolution = await resolveParameter(ref, environment, project.provider, keys);
2061
2204
  const value = requireValue3(resolution, environment);
2062
2205
  if (value === void 0) {
2063
- throw new PenvError9(
2206
+ throw new PenvError10(
2064
2207
  "PARAMETER_ABSENT",
2065
2208
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
2066
2209
  `Set it with \`penv set ${options.key} --env ${environment}\`, or run \`penv get ${options.key} --env ${environment} --explain\` to see every file penv looked at.`
@@ -2110,7 +2253,7 @@ function renderExplain(explanation) {
2110
2253
  ...columns(rows).map((line) => ` ${line}`)
2111
2254
  ];
2112
2255
  }
2113
- var getCommand = defineCommand7({
2256
+ var getCommand = defineCommand8({
2114
2257
  meta: { name: "get", description: "Read a parameter" },
2115
2258
  args: {
2116
2259
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -2145,7 +2288,7 @@ import {
2145
2288
  isReservedToken as isReservedToken3,
2146
2289
  loadConfigFrom as loadConfigFrom2,
2147
2290
  lookupEnvironment,
2148
- PenvError as PenvError11,
2291
+ PenvError as PenvError12,
2149
2292
  parseDotenv,
2150
2293
  ReservedTokenError as ReservedTokenError3,
2151
2294
  refFromVariable,
@@ -2154,7 +2297,7 @@ import {
2154
2297
  UnknownEnvironmentError,
2155
2298
  variableName as variableName5
2156
2299
  } from "@penvhq/core";
2157
- import { defineCommand as defineCommand9 } from "citty";
2300
+ import { defineCommand as defineCommand10 } from "citty";
2158
2301
 
2159
2302
  // src/detect.ts
2160
2303
  import { existsSync, readFileSync } from "fs";
@@ -2260,18 +2403,18 @@ function hasImportsBlock(cwd) {
2260
2403
  // src/commands/init.ts
2261
2404
  import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2262
2405
  import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
2263
- import { createInterface } from "readline/promises";
2406
+ import { createInterface as createInterface2 } from "readline/promises";
2264
2407
  import {
2265
2408
  DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
2266
2409
  isLegalEnvironmentName,
2267
2410
  loadConfigFrom,
2268
- PenvError as PenvError10,
2411
+ PenvError as PenvError11,
2269
2412
  RESERVED_TOKENS,
2270
2413
  schemaFileOf as schemaFileOf2,
2271
2414
  schemaInsideTree,
2272
2415
  validateSchemaFile
2273
2416
  } from "@penvhq/core";
2274
- import { defineCommand as defineCommand8 } from "citty";
2417
+ import { defineCommand as defineCommand9 } from "citty";
2275
2418
  var CONFIG_FILE = "penv.config.ts";
2276
2419
  var TSCONFIG_FILE = "tsconfig.json";
2277
2420
  var GITIGNORE_FILE = ".gitignore";
@@ -2309,7 +2452,7 @@ function suggestEnvironments(root) {
2309
2452
  return [...found].sort();
2310
2453
  }
2311
2454
  function emptyFlag(flag) {
2312
- return new PenvError10(
2455
+ return new PenvError11(
2313
2456
  "INIT_FLAG_EMPTY",
2314
2457
  `\`--${flag}\` was given without a value`,
2315
2458
  flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${DEFAULT_SCHEMA_FILE2}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
@@ -2372,7 +2515,7 @@ function planInit(root, flags = {}) {
2372
2515
  throw emptyFlag("alias");
2373
2516
  }
2374
2517
  if (!ALIAS_NAME.test(alias)) {
2375
- throw new PenvError10(
2518
+ throw new PenvError11(
2376
2519
  "INIT_ALIAS_INVALID",
2377
2520
  `\`${alias}\` is not an alias penv can write`,
2378
2521
  `An alias is \`@name\` \u2014 a tsconfig \`paths\` entry a bundler resolves \u2014 or \`#name\`, a package.json \`imports\` entry Node resolves itself. Those are the two things a module specifier can be that is not a package.`
@@ -2659,7 +2802,7 @@ ${objectIndent}${source.slice(close)}`;
2659
2802
  ${entryIndent}${member},${source.slice(open + 1)}`;
2660
2803
  }
2661
2804
  function shapeError(what, target, alias) {
2662
- return new PenvError10(
2805
+ return new PenvError11(
2663
2806
  "TSCONFIG_SHAPE",
2664
2807
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
2665
2808
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
@@ -2853,7 +2996,7 @@ function renderInit(result2) {
2853
2996
  ];
2854
2997
  }
2855
2998
  async function askOnTty(plan2) {
2856
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2999
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2857
3000
  try {
2858
3001
  return await promptForDecisions(plan2, {
2859
3002
  ask: (question) => rl.question(question),
@@ -2864,7 +3007,7 @@ async function askOnTty(plan2) {
2864
3007
  rl.close();
2865
3008
  }
2866
3009
  }
2867
- var initCommand = defineCommand8({
3010
+ var initCommand = defineCommand9({
2868
3011
  meta: { name: "init", description: "Initialize a project (.penv/, env.ts, config, @env alias)" },
2869
3012
  args: {
2870
3013
  yes: {
@@ -2938,7 +3081,7 @@ function assertImportable(ref, variable) {
2938
3081
  if (!ref.name.includes(".")) {
2939
3082
  return;
2940
3083
  }
2941
- throw new PenvError11(
3084
+ throw new PenvError12(
2942
3085
  "IMPORT_UNPARSEABLE_NAME",
2943
3086
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
2944
3087
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
@@ -2957,7 +3100,7 @@ function assertRoundTrips(ref, variable, config) {
2957
3100
  return;
2958
3101
  }
2959
3102
  const generated = variableName5(ref, config);
2960
- throw new PenvError11(
3103
+ throw new PenvError12(
2961
3104
  "IMPORT_LOSSY_NAME",
2962
3105
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
2963
3106
  `\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`names\` block of penv.config.ts \u2014 \`names: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
@@ -3041,7 +3184,7 @@ function explicitEnvironment(options, source, config) {
3041
3184
  if (value.length > 0) {
3042
3185
  return value;
3043
3186
  }
3044
- throw new PenvError11(
3187
+ throw new PenvError12(
3045
3188
  "IMPORT_ENV_FLAG_EMPTY",
3046
3189
  `\`--env\` for the import of ${source} names no environment`,
3047
3190
  `Pass a declared environment \u2014 ${config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
@@ -3051,7 +3194,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
3051
3194
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
3052
3195
  return;
3053
3196
  }
3054
- throw new PenvError11(
3197
+ throw new PenvError12(
3055
3198
  "IMPORT_ENV_CONFLICT",
3056
3199
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
3057
3200
  `Drop \`--env\` to import ${source} as ${derived}, pass \`--env ${derived}\` to say the same thing twice, or point \`penv import\` at the file that holds ${explicit}'s values. Nothing was imported.`
@@ -3095,7 +3238,7 @@ function importDotenv(options) {
3095
3238
  const cwd = resolve5(options.cwd);
3096
3239
  const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
3097
3240
  if (!existsSync3(file)) {
3098
- throw new PenvError11(
3241
+ throw new PenvError12(
3099
3242
  "IMPORT_FILE_MISSING",
3100
3243
  `There is no file at ${file} to import`,
3101
3244
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
@@ -3208,7 +3351,7 @@ function renderImport(result2, validation) {
3208
3351
  lines.push("", "Done. .penv/ is now your source of truth.");
3209
3352
  return lines;
3210
3353
  }
3211
- var importCommand = defineCommand9({
3354
+ var importCommand = defineCommand10({
3212
3355
  meta: {
3213
3356
  name: "import",
3214
3357
  description: "Import an existing dotenv file; it becomes the source of truth"
@@ -3240,8 +3383,8 @@ var importCommand = defineCommand9({
3240
3383
 
3241
3384
  // src/commands/key.ts
3242
3385
  import { randomBytes } from "crypto";
3243
- import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError12 } from "@penvhq/core";
3244
- import { defineCommand as defineCommand10 } from "citty";
3386
+ import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError13 } from "@penvhq/core";
3387
+ import { defineCommand as defineCommand11 } from "citty";
3245
3388
 
3246
3389
  // src/keychain.ts
3247
3390
  import { createRequire as createRequire2 } from "module";
@@ -3273,7 +3416,7 @@ function runKeyCreate(options) {
3273
3416
  const environment = targetEnvironment(project, options.environment);
3274
3417
  const declared = project.config.keys?.[environment];
3275
3418
  if (declared === void 0) {
3276
- throw new PenvError12(
3419
+ throw new PenvError13(
3277
3420
  "KEY_SOURCE_UNDECLARED",
3278
3421
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
3279
3422
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
@@ -3287,14 +3430,14 @@ function runKeyCreate(options) {
3287
3430
  try {
3288
3431
  existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
3289
3432
  } catch (cause) {
3290
- throw new PenvError12(
3433
+ throw new PenvError13(
3291
3434
  "KEYCHAIN_UNAVAILABLE",
3292
3435
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
3293
3436
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
3294
3437
  );
3295
3438
  }
3296
3439
  if (existing !== null) {
3297
- throw new PenvError12(
3440
+ throw new PenvError13(
3298
3441
  "KEY_EXISTS",
3299
3442
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
3300
3443
  "Replacing it would orphan every value already sealed under it \u2014 they could never be decrypted again. Re-run with `--force` only if you are certain nothing is sealed under the current key."
@@ -3331,10 +3474,10 @@ function renderKeyCreate(result2) {
3331
3474
  "without it, and penv keeps no copy to fall back on."
3332
3475
  ];
3333
3476
  }
3334
- var keyCommand = defineCommand10({
3477
+ var keyCommand = defineCommand11({
3335
3478
  meta: { name: "key", description: "Work with encryption keys" },
3336
3479
  subCommands: {
3337
- create: defineCommand10({
3480
+ create: defineCommand11({
3338
3481
  meta: { name: "create", description: "Generate a key for an environment" },
3339
3482
  args: {
3340
3483
  env: { type: "string", description: "The environment the key is for" },
@@ -3362,7 +3505,7 @@ var keyCommand = defineCommand10({
3362
3505
 
3363
3506
  // src/commands/list.ts
3364
3507
  import { assertNever as assertNever3, resolveAll as resolveAll3, variableName as variableName6 } from "@penvhq/core";
3365
- import { defineCommand as defineCommand11 } from "citty";
3508
+ import { defineCommand as defineCommand12 } from "citty";
3366
3509
  function scopeLabel2(scope) {
3367
3510
  switch (scope.kind) {
3368
3511
  case "environment":
@@ -3402,7 +3545,7 @@ function renderList(result2) {
3402
3545
  }
3403
3546
  return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3404
3547
  }
3405
- var listCommand = defineCommand11({
3548
+ var listCommand = defineCommand12({
3406
3549
  meta: { name: "list", description: "List parameters" },
3407
3550
  args: {
3408
3551
  env: { type: "string", description: "The environment to resolve against" },
@@ -3425,11 +3568,11 @@ import {
3425
3568
  formatMetaFile,
3426
3569
  formatValueFile as formatValueFile5,
3427
3570
  openValue as openValue4,
3428
- PenvError as PenvError13,
3571
+ PenvError as PenvError14,
3429
3572
  parameterId as parameterId5,
3430
3573
  sealValue as sealValue3
3431
3574
  } from "@penvhq/core";
3432
- import { defineCommand as defineCommand12 } from "citty";
3575
+ import { defineCommand as defineCommand13 } from "citty";
3433
3576
  function environmentOf2(file) {
3434
3577
  const scope = file.scope;
3435
3578
  return scope.kind === "environment" || scope.kind === "environment-local" ? scope.environment : void 0;
@@ -3444,7 +3587,7 @@ async function planFile(project, source, target, parameter) {
3444
3587
  }
3445
3588
  const environment = environmentOf2(source);
3446
3589
  if (environment === void 0) {
3447
- throw new PenvError13(
3590
+ throw new PenvError14(
3448
3591
  "SECRET_SCOPE_AMBIGUOUS",
3449
3592
  `${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3450
3593
  "Keys are declared per environment in the `keys` block of penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
@@ -3453,7 +3596,7 @@ async function planFile(project, source, target, parameter) {
3453
3596
  const keys = keySourceFor(project, environment);
3454
3597
  const opened = openValue4(source, stored, keys);
3455
3598
  if (opened.kind === "failed") {
3456
- throw new PenvError13(
3599
+ throw new PenvError14(
3457
3600
  "VALUE_UNDECRYPTABLE",
3458
3601
  `${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3459
3602
  "A sealed value is bound to the file it lives in, so moving it means opening it and sealing it again. Make the key available and run this again. Nothing has been moved."
@@ -3473,9 +3616,10 @@ function filesOf(all, ref) {
3473
3616
  async function runMove(options) {
3474
3617
  const project = openProject(options.cwd);
3475
3618
  const from = refFromKey(options.from, project.config);
3619
+ assertWritableKey(options.to);
3476
3620
  const to = refFromKey(options.to, project.config);
3477
3621
  if (parameterId5(from) === parameterId5(to)) {
3478
- throw new PenvError13(
3622
+ throw new PenvError14(
3479
3623
  "PARAMETER_UNCHANGED",
3480
3624
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
3481
3625
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -3485,7 +3629,7 @@ async function runMove(options) {
3485
3629
  const sources = filesOf(all, from);
3486
3630
  const meta = await project.provider.readMeta(from);
3487
3631
  if (sources.length === 0 && meta === void 0) {
3488
- throw new PenvError13(
3632
+ throw new PenvError14(
3489
3633
  "PARAMETER_ABSENT",
3490
3634
  `Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
3491
3635
  `\`penv list\` shows every parameter penv holds.`
@@ -3493,7 +3637,7 @@ async function runMove(options) {
3493
3637
  }
3494
3638
  const occupied2 = filesOf(all, to);
3495
3639
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
3496
- throw new PenvError13(
3640
+ throw new PenvError14(
3497
3641
  "PARAMETER_EXISTS",
3498
3642
  `Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
3499
3643
  `Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
@@ -3552,7 +3696,7 @@ function renderMove(result2) {
3552
3696
  );
3553
3697
  return lines;
3554
3698
  }
3555
- var mvCommand = defineCommand12({
3699
+ var mvCommand = defineCommand13({
3556
3700
  meta: { name: "mv", description: "Rename a parameter, every scope and its meta at once" },
3557
3701
  args: {
3558
3702
  from: {
@@ -3574,7 +3718,7 @@ var mvCommand = defineCommand12({
3574
3718
  });
3575
3719
 
3576
3720
  // src/commands/pull.ts
3577
- import { defineCommand as defineCommand13 } from "citty";
3721
+ import { defineCommand as defineCommand14 } from "citty";
3578
3722
  async function runPull(options) {
3579
3723
  const project = openProject(options.cwd);
3580
3724
  const environment = targetEnvironment(project, options.environment);
@@ -3631,7 +3775,7 @@ function renderPull(result2) {
3631
3775
  }
3632
3776
  ]);
3633
3777
  }
3634
- var pullCommand = defineCommand13({
3778
+ var pullCommand = defineCommand14({
3635
3779
  meta: {
3636
3780
  name: "pull",
3637
3781
  description: "Materialise the local .penv tree from an environment's source-of-truth provider"
@@ -3652,7 +3796,7 @@ var pullCommand = defineCommand13({
3652
3796
 
3653
3797
  // src/commands/remove.ts
3654
3798
  import { formatValueFile as formatValueFile6 } from "@penvhq/core";
3655
- import { defineCommand as defineCommand14 } from "citty";
3799
+ import { defineCommand as defineCommand15 } from "citty";
3656
3800
  async function runRemove(options) {
3657
3801
  const project = openProject(options.cwd);
3658
3802
  const ref = refFromKey(options.key);
@@ -3696,7 +3840,7 @@ function renderRemove(result2) {
3696
3840
  }));
3697
3841
  return formatRows(rows);
3698
3842
  }
3699
- var removeCommand = defineCommand14({
3843
+ var removeCommand = defineCommand15({
3700
3844
  meta: { name: "remove", description: "Delete a parameter" },
3701
3845
  args: {
3702
3846
  key: { type: "positional", required: true, description: "The parameter, e.g. redis/password" },
@@ -3726,11 +3870,11 @@ var removeCommand = defineCommand14({
3726
3870
  import {
3727
3871
  beginRotation,
3728
3872
  completeRotation,
3729
- PenvError as PenvError14,
3873
+ PenvError as PenvError15,
3730
3874
  retainsPrevious,
3731
3875
  rotationOf as rotationOf2
3732
3876
  } from "@penvhq/core";
3733
- import { defineCommand as defineCommand15 } from "citty";
3877
+ import { defineCommand as defineCommand16 } from "citty";
3734
3878
  function rotatingFile(ref, environment) {
3735
3879
  return {
3736
3880
  namespace: ref.namespace,
@@ -3755,7 +3899,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
3755
3899
  }
3756
3900
  function requireNewValue(value, phase, key) {
3757
3901
  if (value === void 0) {
3758
- throw new PenvError14(
3902
+ throw new PenvError15(
3759
3903
  "ROTATION_NO_VALUE",
3760
3904
  `A ${phase} rotation of ${key} writes a new value, and none was given`,
3761
3905
  "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
@@ -3765,7 +3909,7 @@ function requireNewValue(value, phase, key) {
3765
3909
  }
3766
3910
  function requireRetaining(provider, environment) {
3767
3911
  if (!retainsPrevious(provider)) {
3768
- throw new PenvError14(
3912
+ throw new PenvError15(
3769
3913
  "ROTATION_NOT_RETAINING",
3770
3914
  `A dual-valid rotation needs the previous value to stay readable during the grace window, and the \`${provider.type}\` provider for environment ${environment} does not retain it`,
3771
3915
  "Point this environment at a provider that keeps prior versions (its `readPrevious` is what penv reads during the window), or, if a momentary overlap is not required, declare the parameter `atomic-cutover` in its meta and flip it in one step."
@@ -3782,7 +3926,7 @@ async function runRotate(options) {
3782
3926
  const before = await provider.readMeta(ref);
3783
3927
  const { mechanism } = rotationOf2(before, environment);
3784
3928
  if (mechanism === void 0) {
3785
- throw new PenvError14(
3929
+ throw new PenvError15(
3786
3930
  "ROTATION_NO_MECHANISM",
3787
3931
  `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
3788
3932
  'Set `rotationMechanism` in the parameter\'s meta to `"dual-valid"` (a grace-window overlap) or `"atomic-cutover"` (a single flip), then run `penv rotate` again.'
@@ -3792,7 +3936,7 @@ async function runRotate(options) {
3792
3936
  const complete = options.complete === true;
3793
3937
  if (mechanism === "atomic-cutover") {
3794
3938
  if (begin || complete) {
3795
- throw new PenvError14(
3939
+ throw new PenvError15(
3796
3940
  "ROTATION_MECHANISM_MISMATCH",
3797
3941
  `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
3798
3942
  "Run `penv rotate <key> <value>` with no phase flag to flip it. `--begin`/`--complete` bracket a dual-valid grace window, which atomic-cutover has none of."
@@ -3806,7 +3950,7 @@ async function runRotate(options) {
3806
3950
  }
3807
3951
  const retaining = requireRetaining(provider, environment);
3808
3952
  if (begin === complete) {
3809
- throw new PenvError14(
3953
+ throw new PenvError15(
3810
3954
  "ROTATION_PHASE_REQUIRED",
3811
3955
  `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
3812
3956
  "`--begin` writes the new value and opens the grace window; `--complete` closes it once every reader has moved to the new value. Run them in that order, one at a time."
@@ -3873,7 +4017,7 @@ function renderRotate(result2) {
3873
4017
  }
3874
4018
  ]);
3875
4019
  }
3876
- var rotateCommand = defineCommand15({
4020
+ var rotateCommand = defineCommand16({
3877
4021
  meta: {
3878
4022
  name: "rotate",
3879
4023
  description: "Rotate a parameter's value by the mechanism its meta declares"
@@ -3912,7 +4056,7 @@ var rotateCommand = defineCommand15({
3912
4056
  import { existsSync as existsSync4, watch } from "fs";
3913
4057
  import { basename as basename2, dirname as dirname4, resolve as resolve6 } from "path";
3914
4058
  import { schemaFileOf as schemaFileOf4, schemaInsideTree as schemaInsideTree2 } from "@penvhq/core";
3915
- import { defineCommand as defineCommand16 } from "citty";
4059
+ import { defineCommand as defineCommand17 } from "citty";
3916
4060
  var DEBOUNCE_MS = 100;
3917
4061
  function runWatch(options) {
3918
4062
  const project = openProject(options.cwd);
@@ -4089,7 +4233,7 @@ function renderDrift(drift, environment) {
4089
4233
  }
4090
4234
  return lines;
4091
4235
  }
4092
- var watchCommand = defineCommand16({
4236
+ var watchCommand = defineCommand17({
4093
4237
  meta: {
4094
4238
  name: "watch",
4095
4239
  description: "Re-validate whenever .penv/ or penv.config.ts changes"
@@ -4128,7 +4272,7 @@ var watchCommand = defineCommand16({
4128
4272
  });
4129
4273
 
4130
4274
  // src/index.ts
4131
- var main = defineCommand17({
4275
+ var main = defineCommand18({
4132
4276
  meta: {
4133
4277
  name: "penv",
4134
4278
  description: "Configuration that shares a data model with your production secret manager"
@@ -4139,6 +4283,7 @@ var main = defineCommand17({
4139
4283
  generate: generateCommand,
4140
4284
  get: getCommand,
4141
4285
  set: setCommand,
4286
+ fill: fillCommand,
4142
4287
  mv: mvCommand,
4143
4288
  pull: pullCommand,
4144
4289
  push: pushCommand,
@@ -4164,6 +4309,7 @@ export {
4164
4309
  insertEnvAlias,
4165
4310
  main,
4166
4311
  renderDoctor,
4312
+ renderFill,
4167
4313
  renderMove,
4168
4314
  renderPull,
4169
4315
  renderPush,
@@ -4173,6 +4319,7 @@ export {
4173
4319
  runDoctor,
4174
4320
  runEncrypt,
4175
4321
  runExplain,
4322
+ runFill,
4176
4323
  runGenerate,
4177
4324
  runGet,
4178
4325
  runInit,