@penvhq/cli 0.3.1 → 0.4.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
@@ -376,29 +376,61 @@ function permitsAbsence(node) {
376
376
  }
377
377
  return void 0;
378
378
  }
379
- function leaves(node, path, inherited, out) {
379
+ function leaves(node, path, inherited, out2) {
380
380
  const own = permitsAbsence(node);
381
381
  const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
382
382
  const shape = shapeOf(unwrap(node));
383
383
  if (shape === void 0) {
384
384
  if (path.length > 0) {
385
- out.push({ path, absencePermitted });
385
+ out2.push({ path, absencePermitted, node });
386
386
  }
387
387
  return;
388
388
  }
389
389
  for (const key of Object.keys(shape)) {
390
- leaves(shape[key], [...path, key], absencePermitted, out);
390
+ leaves(shape[key], [...path, key], absencePermitted, out2);
391
391
  }
392
392
  }
393
393
  function combine(left, right) {
394
394
  return left === void 0 || right === void 0 ? void 0 : false;
395
395
  }
396
396
  function declaredLeaves(schema) {
397
- const out = [];
398
- leaves(schema, [], false, out);
399
- return out;
397
+ const out2 = [];
398
+ leaves(schema, [], false, out2);
399
+ return out2;
400
400
  }
401
- var EMPTY_DRIFT = { declared: [], undeclared: [] };
401
+ function defaultValueOf(node) {
402
+ let current = node;
403
+ for (let depth = 0; depth < 8; depth += 1) {
404
+ const def = defOf(current);
405
+ if (def === void 0) {
406
+ return void 0;
407
+ }
408
+ if (typeOf(current) === "default" || typeOf(current) === "prefault") {
409
+ let value = def.defaultValue;
410
+ if (typeof value === "function") {
411
+ try {
412
+ value = value();
413
+ } catch {
414
+ return void 0;
415
+ }
416
+ }
417
+ if (typeof value === "string") {
418
+ return JSON.stringify(value);
419
+ }
420
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
421
+ return String(value);
422
+ }
423
+ return void 0;
424
+ }
425
+ const inner = def.innerType;
426
+ if (inner === void 0) {
427
+ return void 0;
428
+ }
429
+ current = inner;
430
+ }
431
+ return void 0;
432
+ }
433
+ var EMPTY_DRIFT = { declared: [], undeclared: [], optional: [] };
402
434
  function hasValue(resolution) {
403
435
  return resolution.winner !== void 0;
404
436
  }
@@ -406,23 +438,39 @@ function computeDrift(input) {
406
438
  const { schema, resolutions, config, environment } = input;
407
439
  const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
408
440
  const declared = [];
441
+ const optional = [];
409
442
  for (const leaf of declaredLeaves(schema)) {
410
- if (leaf.absencePermitted !== false) {
443
+ if (leaf.absencePermitted === void 0) {
411
444
  continue;
412
445
  }
413
446
  const path = leaf.path.join(".");
414
447
  const ref = refFromAccessPath2(leaf.path);
448
+ 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.`;
415
449
  if (ref === void 0 || isReservedToken2(ref.name, config)) {
416
- declared.push({
417
- subject: path,
418
- remedy: `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.`,
419
- detail: "declared, no filename reaches it"
420
- });
450
+ if (leaf.absencePermitted) {
451
+ optional.push({ subject: path, remedy: renameRemedy });
452
+ } else {
453
+ declared.push({
454
+ subject: path,
455
+ remedy: renameRemedy,
456
+ detail: "declared, no filename reaches it"
457
+ });
458
+ }
421
459
  continue;
422
460
  }
423
461
  if (valued.has(parameterId2(ref))) {
424
462
  continue;
425
463
  }
464
+ if (leaf.absencePermitted) {
465
+ const defaultValue = defaultValueOf(leaf.node);
466
+ optional.push({
467
+ subject: parameterId2(ref),
468
+ ref,
469
+ ...defaultValue === void 0 ? {} : { defaultValue },
470
+ remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
471
+ });
472
+ continue;
473
+ }
426
474
  declared.push({
427
475
  subject: parameterId2(ref),
428
476
  ref,
@@ -440,28 +488,91 @@ function computeDrift(input) {
440
488
  variable: variableName(resolution.ref, config)
441
489
  });
442
490
  }
443
- return { declared, undeclared };
491
+ return { declared, undeclared, optional };
492
+ }
493
+
494
+ // src/style.ts
495
+ function supportsColor(stream) {
496
+ const env = process.env;
497
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") {
498
+ return false;
499
+ }
500
+ if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") {
501
+ return true;
502
+ }
503
+ if (env.TERM === "dumb") {
504
+ return false;
505
+ }
506
+ return stream.isTTY === true;
507
+ }
508
+ var ESC = `${String.fromCharCode(27)}[`;
509
+ var identity = (text) => text;
510
+ function sgr(open, close, on) {
511
+ if (!on) {
512
+ return identity;
513
+ }
514
+ const opener = `${ESC}${open}m`;
515
+ const closer = `${ESC}${close}m`;
516
+ return (text) => `${opener}${text}${closer}`;
517
+ }
518
+ function paletteFor(stream) {
519
+ const on = supportsColor(stream);
520
+ return {
521
+ enabled: on,
522
+ bold: sgr(1, 22, on),
523
+ dim: sgr(2, 22, on),
524
+ red: sgr(31, 39, on),
525
+ green: sgr(32, 39, on),
526
+ yellow: sgr(33, 39, on),
527
+ cyan: sgr(36, 39, on),
528
+ magenta: sgr(35, 39, on)
529
+ };
530
+ }
531
+ var out = paletteFor(process.stdout);
532
+ var err = paletteFor(process.stderr);
533
+ var STYLE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
534
+ function visibleWidth(text) {
535
+ return text.replace(STYLE_PATTERN, "").length;
536
+ }
537
+ function padVisible(text, width) {
538
+ const missing = width - visibleWidth(text);
539
+ return missing > 0 ? text + " ".repeat(missing) : text;
444
540
  }
445
541
 
446
542
  // src/ui.ts
447
543
  import { PenvError as PenvError3 } from "@penvhq/core";
448
544
  var CHECK = "\u2713";
449
545
  var WARN = "\u26A0";
546
+ var CROSS = "\u2717";
450
547
  var UNKNOWN = "?";
548
+ function paintGlyph(glyph) {
549
+ switch (glyph) {
550
+ case CHECK:
551
+ return out.green(glyph);
552
+ case WARN:
553
+ return out.yellow(glyph);
554
+ case CROSS:
555
+ return out.red(glyph);
556
+ case UNKNOWN:
557
+ return out.dim(glyph);
558
+ default:
559
+ return glyph;
560
+ }
561
+ }
451
562
  var NOTE_COLUMN = 29;
452
563
  function widest(values) {
453
- return values.reduce((max, value) => Math.max(max, value.length), 0);
564
+ return values.reduce((max, value) => Math.max(max, visibleWidth(value)), 0);
454
565
  }
455
566
  function formatRows(rows) {
456
567
  const labelWidth = widest(rows.map((row) => row.label)) + 2;
457
568
  const detailed = rows.filter((row) => row.detail !== void 0);
458
569
  const subjectWidth = widest(detailed.map((row) => row.subject ?? "")) + 1;
459
570
  return rows.map((row) => {
460
- const head = `${row.glyph} ${row.label.padEnd(labelWidth)}`;
571
+ const head = `${paintGlyph(row.glyph)} ${padVisible(row.label, labelWidth)}`;
461
572
  if (row.detail === void 0) {
462
573
  return `${head}${row.subject ?? ""}`.trimEnd();
463
574
  }
464
- return `${head}${(row.subject ?? "").padEnd(subjectWidth)}${row.detail}`.trimEnd();
575
+ return `${head}${padVisible(row.subject ?? "", subjectWidth)}${out.dim(row.detail)}`.trimEnd();
465
576
  });
466
577
  }
467
578
  function columns(rows, gap = 2) {
@@ -471,18 +582,31 @@ function columns(rows, gap = 2) {
471
582
  widths.push(widest(rows.map((row) => row[column] ?? "")) + gap);
472
583
  }
473
584
  return rows.map(
474
- (row) => row.map((cell, index) => index === row.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)).join("").trimEnd()
585
+ (row) => row.map(
586
+ (cell, index) => index === row.length - 1 ? cell : padVisible(cell, widths[index] ?? 0)
587
+ ).join("").trimEnd()
475
588
  );
476
589
  }
477
590
  function formatSteps(steps) {
478
591
  return steps.map((step) => {
479
592
  if (step.note === void 0) {
480
- return `${step.glyph} ${step.text}`;
593
+ return `${paintGlyph(step.glyph)} ${step.text}`;
481
594
  }
482
- const text = step.text.length >= NOTE_COLUMN ? `${step.text} ` : step.text.padEnd(NOTE_COLUMN);
483
- return `${step.glyph} ${text}${step.note}`;
595
+ const text = visibleWidth(step.text) >= NOTE_COLUMN ? `${step.text} ` : padVisible(step.text, NOTE_COLUMN);
596
+ return `${paintGlyph(step.glyph)} ${text}${out.dim(step.note)}`;
484
597
  });
485
598
  }
599
+ function heading(command, context) {
600
+ return context === void 0 ? out.bold(command) : `${out.bold(command)} ${out.dim(`\xB7 ${context}`)}`;
601
+ }
602
+ function tip(text) {
603
+ return ` ${out.cyan("\u2192")} ${text}`;
604
+ }
605
+ function prompt(subject, context) {
606
+ const head = `${out.cyan("?")} ${out.bold(subject)}`;
607
+ const tail = out.dim("\u203A ");
608
+ return context === void 0 ? `${head} ${tail}` : `${head} ${out.dim(`\xB7 ${context}`)} ${tail}`;
609
+ }
486
610
  function write(lines) {
487
611
  for (const line of lines) {
488
612
  process.stdout.write(`${line}
@@ -491,13 +615,26 @@ function write(lines) {
491
615
  }
492
616
  function reportError(error) {
493
617
  if (error instanceof PenvError3) {
494
- process.stderr.write(`${error.message}
618
+ const suffix = error.remedy === void 0 ? void 0 : `
619
+ ${error.remedy}`;
620
+ const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
621
+ process.stderr.write(`${err.red(CROSS)} ${message}
495
622
  `);
623
+ if (error.remedy !== void 0) {
624
+ process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
625
+ `);
626
+ }
496
627
  } else if (error instanceof Error) {
497
- process.stderr.write(`${error.stack ?? error.message}
628
+ process.stderr.write(`${err.red(CROSS)} ${error.message}
498
629
  `);
630
+ const stack = error.stack;
631
+ const frames = stack?.startsWith(`${error.name}: ${error.message}`) ? stack.slice(`${error.name}: ${error.message}`.length).replace(/^\n/, "") : stack;
632
+ if (frames !== void 0 && frames !== "") {
633
+ process.stderr.write(`${err.dim(frames)}
634
+ `);
635
+ }
499
636
  } else {
500
- process.stderr.write(`${String(error)}
637
+ process.stderr.write(`${err.red(CROSS)} ${String(error)}
501
638
  `);
502
639
  }
503
640
  process.exitCode = 1;
@@ -688,6 +825,7 @@ import {
688
825
  PenvError as PenvError5,
689
826
  ReservedTokenError as ReservedTokenError2,
690
827
  resolveAll,
828
+ SCHEMA_HARVEST_ENV,
691
829
  schemaFileOf,
692
830
  validateConfig
693
831
  } from "@penvhq/core";
@@ -772,7 +910,9 @@ function loadSchema(project, environment) {
772
910
  async function loadSchemaExclusively(file, schemaPath, environment) {
773
911
  const jiti = jitiFor(file);
774
912
  const previous = process.env.PENV_ENV;
913
+ const previousHarvest = process.env[SCHEMA_HARVEST_ENV];
775
914
  process.env.PENV_ENV = environment;
915
+ process.env[SCHEMA_HARVEST_ENV] = "1";
776
916
  let loaded;
777
917
  try {
778
918
  loaded = await jiti.import(file);
@@ -798,6 +938,11 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
798
938
  } else {
799
939
  process.env.PENV_ENV = previous;
800
940
  }
941
+ if (previousHarvest === void 0) {
942
+ delete process.env[SCHEMA_HARVEST_ENV];
943
+ } else {
944
+ process.env[SCHEMA_HARVEST_ENV] = previousHarvest;
945
+ }
801
946
  }
802
947
  const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
803
948
  if (!isZodType(exported)) {
@@ -897,18 +1042,26 @@ function renderValidate(result2) {
897
1042
  ]);
898
1043
  }
899
1044
  const rows = result2.issues.map((issue) => ({
900
- glyph: WARN,
1045
+ glyph: CROSS,
901
1046
  label: LABELS[issue.kind],
902
1047
  subject: issue.subject,
903
1048
  detail: issue.message
904
1049
  }));
905
1050
  const lines = formatRows(rows);
906
- const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
907
- for (const remedy of remedies) {
908
- if (remedy !== void 0) {
909
- lines.push(` ${remedy}`);
1051
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))].filter(
1052
+ (remedy) => remedy !== void 0
1053
+ );
1054
+ if (remedies.length > 0) {
1055
+ lines.push("");
1056
+ for (const remedy of remedies) {
1057
+ lines.push(tip(remedy));
910
1058
  }
911
1059
  }
1060
+ const count = result2.issues.length;
1061
+ lines.push(
1062
+ "",
1063
+ `${out.red(CROSS)} ${count} ${count === 1 ? "issue" : "issues"} for environment ${result2.environment}`
1064
+ );
912
1065
  return lines;
913
1066
  }
914
1067
  var validateCommand = defineCommand2({
@@ -1663,22 +1816,51 @@ async function providerDriftFindings(project, environment, override) {
1663
1816
  }
1664
1817
  ];
1665
1818
  }
1819
+ var GLYPHS = {
1820
+ pass: CHECK,
1821
+ warning: WARN,
1822
+ failure: CROSS,
1823
+ unknown: UNKNOWN
1824
+ };
1825
+ function summarize(report) {
1826
+ const counts = { pass: 0, warning: 0, failure: 0, unknown: 0 };
1827
+ for (const finding of report.findings) {
1828
+ counts[finding.severity] += 1;
1829
+ }
1830
+ const parts = [out.green(`${counts.pass} passed`)];
1831
+ if (counts.warning > 0) {
1832
+ parts.push(out.yellow(`${counts.warning} ${counts.warning === 1 ? "warning" : "warnings"}`));
1833
+ }
1834
+ if (counts.failure > 0) {
1835
+ parts.push(out.red(`${counts.failure} ${counts.failure === 1 ? "failure" : "failures"}`));
1836
+ }
1837
+ if (counts.unknown > 0) {
1838
+ parts.push(out.dim(`${counts.unknown} could not be checked`));
1839
+ }
1840
+ const verdict = report.ok ? out.green(CHECK) : out.red(CROSS);
1841
+ return `${verdict} ${report.findings.length} checks: ${parts.join(out.dim(" \xB7 "))}`;
1842
+ }
1666
1843
  function renderDoctor(report) {
1667
1844
  const rows = report.findings.map((finding) => ({
1668
- glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
1845
+ glyph: GLYPHS[finding.severity],
1669
1846
  label: finding.label,
1670
1847
  ...finding.subject === void 0 ? {} : { subject: finding.subject },
1671
1848
  ...finding.detail === void 0 ? {} : { detail: finding.detail }
1672
1849
  }));
1673
- const lines = formatRows(rows);
1850
+ const lines = [heading("penv doctor", `environment ${report.environment}`), ""];
1851
+ lines.push(...formatRows(rows));
1674
1852
  const remedies = [
1675
1853
  ...new Set(
1676
1854
  report.findings.filter((finding) => finding.severity !== "pass").map((finding) => finding.remedy).filter((remedy) => remedy !== void 0)
1677
1855
  )
1678
1856
  ];
1679
- for (const remedy of remedies) {
1680
- lines.push(` ${remedy}`);
1857
+ if (remedies.length > 0) {
1858
+ lines.push("");
1859
+ for (const remedy of remedies) {
1860
+ lines.push(tip(remedy));
1861
+ }
1681
1862
  }
1863
+ lines.push("", summarize(report));
1682
1864
  return lines;
1683
1865
  }
1684
1866
  var doctorCommand = defineCommand3({
@@ -1976,9 +2158,52 @@ var decryptCommand = defineCommand5({
1976
2158
  });
1977
2159
 
1978
2160
  // src/commands/fill.ts
1979
- import { createInterface } from "readline/promises";
1980
2161
  import { PenvError as PenvError8 } from "@penvhq/core";
1981
2162
  import { defineCommand as defineCommand6 } from "citty";
2163
+
2164
+ // src/input.ts
2165
+ import { createInterface } from "readline";
2166
+ function lineReader(input = process.stdin, output = process.stdout) {
2167
+ const rl = createInterface({ input, output });
2168
+ const buffered = [];
2169
+ const waiting = [];
2170
+ let closed = false;
2171
+ rl.on("line", (line) => {
2172
+ const next = waiting.shift();
2173
+ if (next === void 0) {
2174
+ buffered.push(line);
2175
+ } else {
2176
+ next(line);
2177
+ }
2178
+ });
2179
+ rl.on("close", () => {
2180
+ closed = true;
2181
+ for (const next of waiting.splice(0)) {
2182
+ next(void 0);
2183
+ }
2184
+ });
2185
+ return {
2186
+ ask(question) {
2187
+ const early = buffered.shift();
2188
+ if (early !== void 0) {
2189
+ return Promise.resolve(early);
2190
+ }
2191
+ if (closed) {
2192
+ return Promise.resolve(void 0);
2193
+ }
2194
+ rl.setPrompt(question);
2195
+ rl.prompt();
2196
+ return new Promise((resolve7) => {
2197
+ waiting.push(resolve7);
2198
+ });
2199
+ },
2200
+ close() {
2201
+ rl.close();
2202
+ }
2203
+ };
2204
+ }
2205
+
2206
+ // src/commands/fill.ts
1982
2207
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1983
2208
  async function runFill(options) {
1984
2209
  const validation = await runValidate({
@@ -2000,6 +2225,7 @@ ${detail}`,
2000
2225
  }
2001
2226
  const written = [];
2002
2227
  const skipped2 = [];
2228
+ const kept = [];
2003
2229
  const unreachable = [];
2004
2230
  for (const drift of validation.drift.declared) {
2005
2231
  if (drift.ref === void 0) {
@@ -2008,7 +2234,12 @@ ${detail}`,
2008
2234
  }
2009
2235
  const ref = drift.ref;
2010
2236
  const key = [...ref.namespace, ref.name].join("/");
2011
- const value = await options.ask({ parameter: key, environment, secret: false });
2237
+ const value = await options.ask({
2238
+ parameter: key,
2239
+ environment,
2240
+ secret: false,
2241
+ optional: false
2242
+ });
2012
2243
  if (value === void 0 || value === "") {
2013
2244
  skipped2.push(drift.subject);
2014
2245
  continue;
@@ -2020,17 +2251,45 @@ ${detail}`,
2020
2251
  encrypted: result2.encrypted
2021
2252
  });
2022
2253
  }
2023
- return { environment, written, skipped: skipped2, unreachable };
2254
+ for (const item of validation.drift.optional) {
2255
+ if (item.ref === void 0) {
2256
+ unreachable.push({ subject: item.subject, remedy: item.remedy });
2257
+ continue;
2258
+ }
2259
+ const ref = item.ref;
2260
+ const key = [...ref.namespace, ref.name].join("/");
2261
+ const value = await options.ask({
2262
+ parameter: key,
2263
+ environment,
2264
+ secret: false,
2265
+ optional: true,
2266
+ ...item.defaultValue === void 0 ? {} : { defaultValue: item.defaultValue }
2267
+ });
2268
+ if (value === void 0 || value === "") {
2269
+ kept.push(item.subject);
2270
+ continue;
2271
+ }
2272
+ const result2 = await runSet({ cwd: options.cwd, key, value, environment });
2273
+ written.push({
2274
+ parameter: item.subject,
2275
+ location: result2.location,
2276
+ encrypted: result2.encrypted
2277
+ });
2278
+ }
2279
+ return { environment, written, skipped: skipped2, kept, unreachable };
2024
2280
  }
2025
2281
  function summaryLine(result2) {
2026
2282
  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`;
2283
+ if (filled === 0 && result2.skipped.length === 0 && result2.kept.length === 0 && result2.unreachable.length === 0) {
2284
+ return `${out.green(CHECK)} Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2029
2285
  }
2030
2286
  const parts = [`${filled} written`];
2031
2287
  if (result2.skipped.length > 0) {
2032
2288
  parts.push(`${result2.skipped.length} skipped`);
2033
2289
  }
2290
+ if (result2.kept.length > 0) {
2291
+ parts.push(`${result2.kept.length} left to the schema's defaults`);
2292
+ }
2034
2293
  if (result2.unreachable.length > 0) {
2035
2294
  parts.push(`${result2.unreachable.length} unreachable`);
2036
2295
  }
@@ -2051,6 +2310,16 @@ function renderFill(result2) {
2051
2310
  lines.push(summaryLine(result2));
2052
2311
  return lines;
2053
2312
  }
2313
+ function contextFor(prompt2) {
2314
+ if (!prompt2.optional) {
2315
+ return `${prompt2.environment}, Enter to skip`;
2316
+ }
2317
+ if (prompt2.defaultValue === void 0) {
2318
+ return `${prompt2.environment} \xB7 optional, Enter leaves it unset`;
2319
+ }
2320
+ const shown = prompt2.defaultValue.length > 24 ? `${prompt2.defaultValue.slice(0, 23)}\u2026` : prompt2.defaultValue;
2321
+ return `${prompt2.environment} \xB7 optional, Enter keeps ${shown}`;
2322
+ }
2054
2323
  var fillCommand = defineCommand6({
2055
2324
  meta: {
2056
2325
  name: "fill",
@@ -2061,8 +2330,8 @@ var fillCommand = defineCommand6({
2061
2330
  },
2062
2331
  run({ args }) {
2063
2332
  return guard(async () => {
2064
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2065
- const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2333
+ const reader = lineReader();
2334
+ const ask = (prompt2) => reader.ask(prompt(prompt2.parameter, contextFor(prompt2)));
2066
2335
  try {
2067
2336
  write(
2068
2337
  renderFill(
@@ -2074,7 +2343,7 @@ var fillCommand = defineCommand6({
2074
2343
  )
2075
2344
  );
2076
2345
  } finally {
2077
- rl.close();
2346
+ reader.close();
2078
2347
  }
2079
2348
  });
2080
2349
  }
@@ -2243,12 +2512,14 @@ async function runExplain(options) {
2243
2512
  function renderExplain(explanation) {
2244
2513
  const target = explanation.location === void 0 ? "nothing" : `${PENV_DIR}/${explanation.location}`;
2245
2514
  const rows = explanation.candidates.map((candidate) => [
2246
- candidate.location,
2247
- candidate.wins ? "present, wins" : candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
2515
+ candidate.wins ? candidate.location : out.dim(candidate.location),
2516
+ candidate.wins ? out.green("present, wins") : out.dim(
2517
+ candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
2518
+ )
2248
2519
  ]);
2249
2520
  return [
2250
- `${explanation.parameter} resolves to ${target} for environment ${explanation.environment}`,
2251
- ...explanation.undecryptable === void 0 ? [] : [` penv cannot decrypt it: ${explanation.undecryptable}`],
2521
+ `${out.bold(explanation.parameter)} resolves to ${target} for environment ${explanation.environment}`,
2522
+ ...explanation.undecryptable === void 0 ? [] : [` ${out.red(CROSS)} penv cannot decrypt it: ${explanation.undecryptable}`],
2252
2523
  "",
2253
2524
  ...columns(rows).map((line) => ` ${line}`)
2254
2525
  ];
@@ -2553,23 +2824,27 @@ function planInit(root, flags = {}) {
2553
2824
  function renderPlan(plan2) {
2554
2825
  const rows = [];
2555
2826
  rows.push([
2556
- " environments",
2557
- plan2.suggestedEnvironments.length === 0 ? "" : `[${plan2.suggestedEnvironments.join(", ")}]`,
2558
- plan2.suggestedEnvironments.length === 0 ? "<- name them, or Enter to leave the whitelist empty" : "<- from your .env files; edit, or Enter to accept"
2827
+ ` ${out.dim("environments")}`,
2828
+ plan2.suggestedEnvironments.length === 0 ? "" : out.cyan(`[${plan2.suggestedEnvironments.join(", ")}]`),
2829
+ out.dim(
2830
+ plan2.suggestedEnvironments.length === 0 ? "\u2190 name them, or Enter to leave the whitelist empty" : "\u2190 from your .env files; edit, or Enter to accept"
2831
+ )
2559
2832
  ]);
2560
2833
  rows.push([
2561
- " schemaFile",
2834
+ ` ${out.dim("schemaFile")}`,
2562
2835
  plan2.decisions.schemaFile,
2563
- plan2.decisions.schemaFile === DEFAULT_SCHEMA_FILE2 ? "" : `(default: ${DEFAULT_SCHEMA_FILE2})`
2836
+ plan2.decisions.schemaFile === DEFAULT_SCHEMA_FILE2 ? "" : out.dim(`(default: ${DEFAULT_SCHEMA_FILE2})`)
2564
2837
  ]);
2565
2838
  for (const prefix of plan2.decisions.publicPrefixes) {
2566
- rows.push([" publicPrefix", prefix, ""]);
2839
+ rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
2567
2840
  }
2568
- const headline = plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`;
2841
+ const headline = out.bold(
2842
+ plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`
2843
+ );
2569
2844
  return [headline, "", ...columns(rows), ""];
2570
2845
  }
2571
2846
  function environmentsHint(plan2) {
2572
- return plan2.suggestedEnvironments.length === 0 ? "environments (comma-separated, Enter for none) > " : 'environments (Enter to accept, "none" for an empty whitelist) > ';
2847
+ return plan2.suggestedEnvironments.length === 0 ? prompt("environments", "comma-separated, Enter for none") : prompt("environments", 'Enter to accept, "none" for an empty whitelist');
2573
2848
  }
2574
2849
  async function promptForDecisions(plan2, io) {
2575
2850
  for (const line of renderPlan(plan2)) {
@@ -2584,7 +2859,7 @@ async function promptForDecisions(plan2, io) {
2584
2859
  );
2585
2860
  io.write("");
2586
2861
  }
2587
- const proceed = (await io.ask("Proceed? [Y/n] ")).trim().toLowerCase();
2862
+ const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
2588
2863
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
2589
2864
  return void 0;
2590
2865
  }
@@ -2989,7 +3264,8 @@ function renderInit(result2) {
2989
3264
  return [
2990
3265
  ...formatSteps(steps),
2991
3266
  "",
2992
- `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
3267
+ `${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${result2.decisions.schemaFile}, then:`,
3268
+ tip(out.cyan("penv set <key>")),
2993
3269
  ...result2.decisions.environments.length === 0 ? [
2994
3270
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2995
3271
  ] : []
@@ -3458,20 +3734,28 @@ function runKeyCreate(options) {
3458
3734
  function renderKeyCreate(result2) {
3459
3735
  if (result2.source === "keychain") {
3460
3736
  return [
3461
- `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
3737
+ `${out.green(CHECK)} A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
3462
3738
  "",
3463
- "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
3464
- "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
3739
+ out.dim(
3740
+ "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running"
3741
+ ),
3742
+ out.dim(
3743
+ "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
3744
+ )
3465
3745
  ];
3466
3746
  }
3467
3747
  return [
3468
- `A new key for environment ${result2.environment}. penv did not store it.`,
3748
+ `${out.green(CHECK)} A new key for environment ${result2.environment}. penv did not store it.`,
3469
3749
  "",
3470
- ` ${result2.variable}=${result2.key}`,
3750
+ ` ${out.cyan(`${result2.variable}=${result2.key}`)}`,
3471
3751
  "",
3472
- "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
3473
- "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
3474
- "without it, and penv keeps no copy to fall back on."
3752
+ out.dim(
3753
+ "Export it where penv runs, and put it wherever this environment's secrets already live \u2014"
3754
+ ),
3755
+ out.dim(
3756
+ "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable"
3757
+ ),
3758
+ out.dim("without it, and penv keeps no copy to fall back on.")
3475
3759
  ];
3476
3760
  }
3477
3761
  var keyCommand = defineCommand11({
@@ -3539,11 +3823,31 @@ async function runList(options) {
3539
3823
  }
3540
3824
  return { environment, parameters };
3541
3825
  }
3826
+ function paintScope(entry) {
3827
+ if (entry.scope === "absent" || entry.viaUnscopedFallback) {
3828
+ return out.yellow(entry.scope);
3829
+ }
3830
+ return out.green(entry.scope);
3831
+ }
3542
3832
  function renderList(result2) {
3543
3833
  if (result2.parameters.length === 0) {
3544
- return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3834
+ return [
3835
+ `No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`,
3836
+ tip(`penv set <key> --env ${result2.environment}`)
3837
+ ];
3545
3838
  }
3546
- return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3839
+ const header = ["parameter", "scope", "variable", ""].map((cell) => out.dim(cell));
3840
+ const rows = result2.parameters.map((entry) => [
3841
+ entry.parameter,
3842
+ paintScope(entry),
3843
+ entry.variable,
3844
+ entry.encrypted ? out.dim("encrypted") : ""
3845
+ ]);
3846
+ return [
3847
+ heading("penv list", `environment ${result2.environment}`),
3848
+ "",
3849
+ ...columns([header, ...rows])
3850
+ ];
3547
3851
  }
3548
3852
  var listCommand = defineCommand12({
3549
3853
  meta: { name: "list", description: "List parameters" },
@@ -3691,8 +3995,9 @@ function renderMove(result2) {
3691
3995
  const lines = formatRows(rows);
3692
3996
  lines.push(
3693
3997
  "",
3694
- ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3695
- " or `penv validate` will report the value as unused and the declaration as unset."
3998
+ tip(
3999
+ `.penv/env.ts still declares \`${result2.schema.was}\` \u2014 rename it to \`${result2.schema.now}\`, or \`penv validate\` will report the value as unused and the declaration as unset.`
4000
+ )
3696
4001
  );
3697
4002
  return lines;
3698
4003
  }
@@ -4227,9 +4532,9 @@ function renderDrift(drift, environment) {
4227
4532
  if (rows.length === 0) {
4228
4533
  return [];
4229
4534
  }
4230
- const lines = ["", `Schema and tree differ for ${environment}:`, ...formatRows(rows)];
4535
+ const lines = ["", out.bold(`Schema and tree differ for ${environment}:`), ...formatRows(rows)];
4231
4536
  for (const remedy of new Set(drift.declared.map((item) => item.remedy))) {
4232
- lines.push(` ${remedy}`);
4537
+ lines.push(tip(remedy));
4233
4538
  }
4234
4539
  return lines;
4235
4540
  }
@@ -4260,7 +4565,7 @@ var watchCommand = defineCommand17({
4260
4565
  process.exitCode = previous;
4261
4566
  }
4262
4567
  });
4263
- write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
4568
+ write([`Watching .penv/ and penv.config.ts. ${out.dim("Ctrl-C to stop.")}`]);
4264
4569
  await new Promise((resolve7) => {
4265
4570
  process.once("SIGINT", () => {
4266
4571
  handle.close();