@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.cjs CHANGED
@@ -396,29 +396,61 @@ function permitsAbsence(node) {
396
396
  }
397
397
  return void 0;
398
398
  }
399
- function leaves(node, path, inherited, out) {
399
+ function leaves(node, path, inherited, out2) {
400
400
  const own = permitsAbsence(node);
401
401
  const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
402
402
  const shape = shapeOf(unwrap(node));
403
403
  if (shape === void 0) {
404
404
  if (path.length > 0) {
405
- out.push({ path, absencePermitted });
405
+ out2.push({ path, absencePermitted, node });
406
406
  }
407
407
  return;
408
408
  }
409
409
  for (const key of Object.keys(shape)) {
410
- leaves(shape[key], [...path, key], absencePermitted, out);
410
+ leaves(shape[key], [...path, key], absencePermitted, out2);
411
411
  }
412
412
  }
413
413
  function combine(left, right) {
414
414
  return left === void 0 || right === void 0 ? void 0 : false;
415
415
  }
416
416
  function declaredLeaves(schema) {
417
- const out = [];
418
- leaves(schema, [], false, out);
419
- return out;
417
+ const out2 = [];
418
+ leaves(schema, [], false, out2);
419
+ return out2;
420
420
  }
421
- var EMPTY_DRIFT = { declared: [], undeclared: [] };
421
+ function defaultValueOf(node) {
422
+ let current = node;
423
+ for (let depth = 0; depth < 8; depth += 1) {
424
+ const def = defOf(current);
425
+ if (def === void 0) {
426
+ return void 0;
427
+ }
428
+ if (typeOf(current) === "default" || typeOf(current) === "prefault") {
429
+ let value = def.defaultValue;
430
+ if (typeof value === "function") {
431
+ try {
432
+ value = value();
433
+ } catch {
434
+ return void 0;
435
+ }
436
+ }
437
+ if (typeof value === "string") {
438
+ return JSON.stringify(value);
439
+ }
440
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
441
+ return String(value);
442
+ }
443
+ return void 0;
444
+ }
445
+ const inner = def.innerType;
446
+ if (inner === void 0) {
447
+ return void 0;
448
+ }
449
+ current = inner;
450
+ }
451
+ return void 0;
452
+ }
453
+ var EMPTY_DRIFT = { declared: [], undeclared: [], optional: [] };
422
454
  function hasValue(resolution) {
423
455
  return resolution.winner !== void 0;
424
456
  }
@@ -426,23 +458,39 @@ function computeDrift(input) {
426
458
  const { schema, resolutions, config, environment } = input;
427
459
  const valued = new Set(resolutions.filter(hasValue).map((resolution) => resolution.parameter));
428
460
  const declared = [];
461
+ const optional = [];
429
462
  for (const leaf of declaredLeaves(schema)) {
430
- if (leaf.absencePermitted !== false) {
463
+ if (leaf.absencePermitted === void 0) {
431
464
  continue;
432
465
  }
433
466
  const path = leaf.path.join(".");
434
467
  const ref = (0, import_core3.refFromAccessPath)(leaf.path);
468
+ 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.`;
435
469
  if (ref === void 0 || (0, import_core3.isReservedToken)(ref.name, config)) {
436
- declared.push({
437
- subject: path,
438
- 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.`,
439
- detail: "declared, no filename reaches it"
440
- });
470
+ if (leaf.absencePermitted) {
471
+ optional.push({ subject: path, remedy: renameRemedy });
472
+ } else {
473
+ declared.push({
474
+ subject: path,
475
+ remedy: renameRemedy,
476
+ detail: "declared, no filename reaches it"
477
+ });
478
+ }
441
479
  continue;
442
480
  }
443
481
  if (valued.has((0, import_core3.parameterId)(ref))) {
444
482
  continue;
445
483
  }
484
+ if (leaf.absencePermitted) {
485
+ const defaultValue = defaultValueOf(leaf.node);
486
+ optional.push({
487
+ subject: (0, import_core3.parameterId)(ref),
488
+ ref,
489
+ ...defaultValue === void 0 ? {} : { defaultValue },
490
+ remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
491
+ });
492
+ continue;
493
+ }
446
494
  declared.push({
447
495
  subject: (0, import_core3.parameterId)(ref),
448
496
  ref,
@@ -460,28 +508,91 @@ function computeDrift(input) {
460
508
  variable: (0, import_core3.variableName)(resolution.ref, config)
461
509
  });
462
510
  }
463
- return { declared, undeclared };
511
+ return { declared, undeclared, optional };
512
+ }
513
+
514
+ // src/style.ts
515
+ function supportsColor(stream) {
516
+ const env = process.env;
517
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") {
518
+ return false;
519
+ }
520
+ if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") {
521
+ return true;
522
+ }
523
+ if (env.TERM === "dumb") {
524
+ return false;
525
+ }
526
+ return stream.isTTY === true;
527
+ }
528
+ var ESC = `${String.fromCharCode(27)}[`;
529
+ var identity = (text) => text;
530
+ function sgr(open, close, on) {
531
+ if (!on) {
532
+ return identity;
533
+ }
534
+ const opener = `${ESC}${open}m`;
535
+ const closer = `${ESC}${close}m`;
536
+ return (text) => `${opener}${text}${closer}`;
537
+ }
538
+ function paletteFor(stream) {
539
+ const on = supportsColor(stream);
540
+ return {
541
+ enabled: on,
542
+ bold: sgr(1, 22, on),
543
+ dim: sgr(2, 22, on),
544
+ red: sgr(31, 39, on),
545
+ green: sgr(32, 39, on),
546
+ yellow: sgr(33, 39, on),
547
+ cyan: sgr(36, 39, on),
548
+ magenta: sgr(35, 39, on)
549
+ };
550
+ }
551
+ var out = paletteFor(process.stdout);
552
+ var err = paletteFor(process.stderr);
553
+ var STYLE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
554
+ function visibleWidth(text) {
555
+ return text.replace(STYLE_PATTERN, "").length;
556
+ }
557
+ function padVisible(text, width) {
558
+ const missing = width - visibleWidth(text);
559
+ return missing > 0 ? text + " ".repeat(missing) : text;
464
560
  }
465
561
 
466
562
  // src/ui.ts
467
563
  var import_core4 = require("@penvhq/core");
468
564
  var CHECK = "\u2713";
469
565
  var WARN = "\u26A0";
566
+ var CROSS = "\u2717";
470
567
  var UNKNOWN = "?";
568
+ function paintGlyph(glyph) {
569
+ switch (glyph) {
570
+ case CHECK:
571
+ return out.green(glyph);
572
+ case WARN:
573
+ return out.yellow(glyph);
574
+ case CROSS:
575
+ return out.red(glyph);
576
+ case UNKNOWN:
577
+ return out.dim(glyph);
578
+ default:
579
+ return glyph;
580
+ }
581
+ }
471
582
  var NOTE_COLUMN = 29;
472
583
  function widest(values) {
473
- return values.reduce((max, value) => Math.max(max, value.length), 0);
584
+ return values.reduce((max, value) => Math.max(max, visibleWidth(value)), 0);
474
585
  }
475
586
  function formatRows(rows) {
476
587
  const labelWidth = widest(rows.map((row) => row.label)) + 2;
477
588
  const detailed = rows.filter((row) => row.detail !== void 0);
478
589
  const subjectWidth = widest(detailed.map((row) => row.subject ?? "")) + 1;
479
590
  return rows.map((row) => {
480
- const head = `${row.glyph} ${row.label.padEnd(labelWidth)}`;
591
+ const head = `${paintGlyph(row.glyph)} ${padVisible(row.label, labelWidth)}`;
481
592
  if (row.detail === void 0) {
482
593
  return `${head}${row.subject ?? ""}`.trimEnd();
483
594
  }
484
- return `${head}${(row.subject ?? "").padEnd(subjectWidth)}${row.detail}`.trimEnd();
595
+ return `${head}${padVisible(row.subject ?? "", subjectWidth)}${out.dim(row.detail)}`.trimEnd();
485
596
  });
486
597
  }
487
598
  function columns(rows, gap = 2) {
@@ -491,18 +602,31 @@ function columns(rows, gap = 2) {
491
602
  widths.push(widest(rows.map((row) => row[column] ?? "")) + gap);
492
603
  }
493
604
  return rows.map(
494
- (row) => row.map((cell, index) => index === row.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)).join("").trimEnd()
605
+ (row) => row.map(
606
+ (cell, index) => index === row.length - 1 ? cell : padVisible(cell, widths[index] ?? 0)
607
+ ).join("").trimEnd()
495
608
  );
496
609
  }
497
610
  function formatSteps(steps) {
498
611
  return steps.map((step) => {
499
612
  if (step.note === void 0) {
500
- return `${step.glyph} ${step.text}`;
613
+ return `${paintGlyph(step.glyph)} ${step.text}`;
501
614
  }
502
- const text = step.text.length >= NOTE_COLUMN ? `${step.text} ` : step.text.padEnd(NOTE_COLUMN);
503
- return `${step.glyph} ${text}${step.note}`;
615
+ const text = visibleWidth(step.text) >= NOTE_COLUMN ? `${step.text} ` : padVisible(step.text, NOTE_COLUMN);
616
+ return `${paintGlyph(step.glyph)} ${text}${out.dim(step.note)}`;
504
617
  });
505
618
  }
619
+ function heading(command, context) {
620
+ return context === void 0 ? out.bold(command) : `${out.bold(command)} ${out.dim(`\xB7 ${context}`)}`;
621
+ }
622
+ function tip(text) {
623
+ return ` ${out.cyan("\u2192")} ${text}`;
624
+ }
625
+ function prompt(subject, context) {
626
+ const head = `${out.cyan("?")} ${out.bold(subject)}`;
627
+ const tail = out.dim("\u203A ");
628
+ return context === void 0 ? `${head} ${tail}` : `${head} ${out.dim(`\xB7 ${context}`)} ${tail}`;
629
+ }
506
630
  function write(lines) {
507
631
  for (const line of lines) {
508
632
  process.stdout.write(`${line}
@@ -511,13 +635,26 @@ function write(lines) {
511
635
  }
512
636
  function reportError(error) {
513
637
  if (error instanceof import_core4.PenvError) {
514
- process.stderr.write(`${error.message}
638
+ const suffix = error.remedy === void 0 ? void 0 : `
639
+ ${error.remedy}`;
640
+ const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
641
+ process.stderr.write(`${err.red(CROSS)} ${message}
642
+ `);
643
+ if (error.remedy !== void 0) {
644
+ process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
515
645
  `);
646
+ }
516
647
  } else if (error instanceof Error) {
517
- process.stderr.write(`${error.stack ?? error.message}
648
+ process.stderr.write(`${err.red(CROSS)} ${error.message}
649
+ `);
650
+ const stack = error.stack;
651
+ const frames = stack?.startsWith(`${error.name}: ${error.message}`) ? stack.slice(`${error.name}: ${error.message}`.length).replace(/^\n/, "") : stack;
652
+ if (frames !== void 0 && frames !== "") {
653
+ process.stderr.write(`${err.dim(frames)}
518
654
  `);
655
+ }
519
656
  } else {
520
- process.stderr.write(`${String(error)}
657
+ process.stderr.write(`${err.red(CROSS)} ${String(error)}
521
658
  `);
522
659
  }
523
660
  process.exitCode = 1;
@@ -782,7 +919,9 @@ function loadSchema(project, environment) {
782
919
  async function loadSchemaExclusively(file, schemaPath, environment) {
783
920
  const jiti = (0, import_core6.jitiFor)(file);
784
921
  const previous = process.env.PENV_ENV;
922
+ const previousHarvest = process.env[import_core6.SCHEMA_HARVEST_ENV];
785
923
  process.env.PENV_ENV = environment;
924
+ process.env[import_core6.SCHEMA_HARVEST_ENV] = "1";
786
925
  let loaded;
787
926
  try {
788
927
  loaded = await jiti.import(file);
@@ -808,6 +947,11 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
808
947
  } else {
809
948
  process.env.PENV_ENV = previous;
810
949
  }
950
+ if (previousHarvest === void 0) {
951
+ delete process.env[import_core6.SCHEMA_HARVEST_ENV];
952
+ } else {
953
+ process.env[import_core6.SCHEMA_HARVEST_ENV] = previousHarvest;
954
+ }
811
955
  }
812
956
  const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
813
957
  if (!isZodType(exported)) {
@@ -907,18 +1051,26 @@ function renderValidate(result2) {
907
1051
  ]);
908
1052
  }
909
1053
  const rows = result2.issues.map((issue) => ({
910
- glyph: WARN,
1054
+ glyph: CROSS,
911
1055
  label: LABELS[issue.kind],
912
1056
  subject: issue.subject,
913
1057
  detail: issue.message
914
1058
  }));
915
1059
  const lines = formatRows(rows);
916
- const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
917
- for (const remedy of remedies) {
918
- if (remedy !== void 0) {
919
- lines.push(` ${remedy}`);
1060
+ const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))].filter(
1061
+ (remedy) => remedy !== void 0
1062
+ );
1063
+ if (remedies.length > 0) {
1064
+ lines.push("");
1065
+ for (const remedy of remedies) {
1066
+ lines.push(tip(remedy));
920
1067
  }
921
1068
  }
1069
+ const count = result2.issues.length;
1070
+ lines.push(
1071
+ "",
1072
+ `${out.red(CROSS)} ${count} ${count === 1 ? "issue" : "issues"} for environment ${result2.environment}`
1073
+ );
922
1074
  return lines;
923
1075
  }
924
1076
  var validateCommand = (0, import_citty2.defineCommand)({
@@ -1673,22 +1825,51 @@ async function providerDriftFindings(project, environment, override) {
1673
1825
  }
1674
1826
  ];
1675
1827
  }
1828
+ var GLYPHS = {
1829
+ pass: CHECK,
1830
+ warning: WARN,
1831
+ failure: CROSS,
1832
+ unknown: UNKNOWN
1833
+ };
1834
+ function summarize(report) {
1835
+ const counts = { pass: 0, warning: 0, failure: 0, unknown: 0 };
1836
+ for (const finding of report.findings) {
1837
+ counts[finding.severity] += 1;
1838
+ }
1839
+ const parts = [out.green(`${counts.pass} passed`)];
1840
+ if (counts.warning > 0) {
1841
+ parts.push(out.yellow(`${counts.warning} ${counts.warning === 1 ? "warning" : "warnings"}`));
1842
+ }
1843
+ if (counts.failure > 0) {
1844
+ parts.push(out.red(`${counts.failure} ${counts.failure === 1 ? "failure" : "failures"}`));
1845
+ }
1846
+ if (counts.unknown > 0) {
1847
+ parts.push(out.dim(`${counts.unknown} could not be checked`));
1848
+ }
1849
+ const verdict = report.ok ? out.green(CHECK) : out.red(CROSS);
1850
+ return `${verdict} ${report.findings.length} checks: ${parts.join(out.dim(" \xB7 "))}`;
1851
+ }
1676
1852
  function renderDoctor(report) {
1677
1853
  const rows = report.findings.map((finding) => ({
1678
- glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
1854
+ glyph: GLYPHS[finding.severity],
1679
1855
  label: finding.label,
1680
1856
  ...finding.subject === void 0 ? {} : { subject: finding.subject },
1681
1857
  ...finding.detail === void 0 ? {} : { detail: finding.detail }
1682
1858
  }));
1683
- const lines = formatRows(rows);
1859
+ const lines = [heading("penv doctor", `environment ${report.environment}`), ""];
1860
+ lines.push(...formatRows(rows));
1684
1861
  const remedies = [
1685
1862
  ...new Set(
1686
1863
  report.findings.filter((finding) => finding.severity !== "pass").map((finding) => finding.remedy).filter((remedy) => remedy !== void 0)
1687
1864
  )
1688
1865
  ];
1689
- for (const remedy of remedies) {
1690
- lines.push(` ${remedy}`);
1866
+ if (remedies.length > 0) {
1867
+ lines.push("");
1868
+ for (const remedy of remedies) {
1869
+ lines.push(tip(remedy));
1870
+ }
1691
1871
  }
1872
+ lines.push("", summarize(report));
1692
1873
  return lines;
1693
1874
  }
1694
1875
  var doctorCommand = (0, import_citty3.defineCommand)({
@@ -1979,9 +2160,52 @@ var decryptCommand = (0, import_citty5.defineCommand)({
1979
2160
  });
1980
2161
 
1981
2162
  // src/commands/fill.ts
1982
- var import_promises = require("readline/promises");
1983
2163
  var import_core10 = require("@penvhq/core");
1984
2164
  var import_citty6 = require("citty");
2165
+
2166
+ // src/input.ts
2167
+ var import_node_readline = require("readline");
2168
+ function lineReader(input = process.stdin, output = process.stdout) {
2169
+ const rl = (0, import_node_readline.createInterface)({ input, output });
2170
+ const buffered = [];
2171
+ const waiting = [];
2172
+ let closed = false;
2173
+ rl.on("line", (line) => {
2174
+ const next = waiting.shift();
2175
+ if (next === void 0) {
2176
+ buffered.push(line);
2177
+ } else {
2178
+ next(line);
2179
+ }
2180
+ });
2181
+ rl.on("close", () => {
2182
+ closed = true;
2183
+ for (const next of waiting.splice(0)) {
2184
+ next(void 0);
2185
+ }
2186
+ });
2187
+ return {
2188
+ ask(question) {
2189
+ const early = buffered.shift();
2190
+ if (early !== void 0) {
2191
+ return Promise.resolve(early);
2192
+ }
2193
+ if (closed) {
2194
+ return Promise.resolve(void 0);
2195
+ }
2196
+ rl.setPrompt(question);
2197
+ rl.prompt();
2198
+ return new Promise((resolve7) => {
2199
+ waiting.push(resolve7);
2200
+ });
2201
+ },
2202
+ close() {
2203
+ rl.close();
2204
+ }
2205
+ };
2206
+ }
2207
+
2208
+ // src/commands/fill.ts
1985
2209
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1986
2210
  async function runFill(options) {
1987
2211
  const validation = await runValidate({
@@ -2003,6 +2227,7 @@ ${detail}`,
2003
2227
  }
2004
2228
  const written = [];
2005
2229
  const skipped2 = [];
2230
+ const kept = [];
2006
2231
  const unreachable = [];
2007
2232
  for (const drift of validation.drift.declared) {
2008
2233
  if (drift.ref === void 0) {
@@ -2011,7 +2236,12 @@ ${detail}`,
2011
2236
  }
2012
2237
  const ref = drift.ref;
2013
2238
  const key = [...ref.namespace, ref.name].join("/");
2014
- const value = await options.ask({ parameter: key, environment, secret: false });
2239
+ const value = await options.ask({
2240
+ parameter: key,
2241
+ environment,
2242
+ secret: false,
2243
+ optional: false
2244
+ });
2015
2245
  if (value === void 0 || value === "") {
2016
2246
  skipped2.push(drift.subject);
2017
2247
  continue;
@@ -2023,17 +2253,45 @@ ${detail}`,
2023
2253
  encrypted: result2.encrypted
2024
2254
  });
2025
2255
  }
2026
- return { environment, written, skipped: skipped2, unreachable };
2256
+ for (const item of validation.drift.optional) {
2257
+ if (item.ref === void 0) {
2258
+ unreachable.push({ subject: item.subject, remedy: item.remedy });
2259
+ continue;
2260
+ }
2261
+ const ref = item.ref;
2262
+ const key = [...ref.namespace, ref.name].join("/");
2263
+ const value = await options.ask({
2264
+ parameter: key,
2265
+ environment,
2266
+ secret: false,
2267
+ optional: true,
2268
+ ...item.defaultValue === void 0 ? {} : { defaultValue: item.defaultValue }
2269
+ });
2270
+ if (value === void 0 || value === "") {
2271
+ kept.push(item.subject);
2272
+ continue;
2273
+ }
2274
+ const result2 = await runSet({ cwd: options.cwd, key, value, environment });
2275
+ written.push({
2276
+ parameter: item.subject,
2277
+ location: result2.location,
2278
+ encrypted: result2.encrypted
2279
+ });
2280
+ }
2281
+ return { environment, written, skipped: skipped2, kept, unreachable };
2027
2282
  }
2028
2283
  function summaryLine(result2) {
2029
2284
  const filled = result2.written.length;
2030
- if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
2031
- return `Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2285
+ if (filled === 0 && result2.skipped.length === 0 && result2.kept.length === 0 && result2.unreachable.length === 0) {
2286
+ return `${out.green(CHECK)} Nothing to fill for environment ${result2.environment}: every declared parameter has a value`;
2032
2287
  }
2033
2288
  const parts = [`${filled} written`];
2034
2289
  if (result2.skipped.length > 0) {
2035
2290
  parts.push(`${result2.skipped.length} skipped`);
2036
2291
  }
2292
+ if (result2.kept.length > 0) {
2293
+ parts.push(`${result2.kept.length} left to the schema's defaults`);
2294
+ }
2037
2295
  if (result2.unreachable.length > 0) {
2038
2296
  parts.push(`${result2.unreachable.length} unreachable`);
2039
2297
  }
@@ -2054,6 +2312,16 @@ function renderFill(result2) {
2054
2312
  lines.push(summaryLine(result2));
2055
2313
  return lines;
2056
2314
  }
2315
+ function contextFor(prompt2) {
2316
+ if (!prompt2.optional) {
2317
+ return `${prompt2.environment}, Enter to skip`;
2318
+ }
2319
+ if (prompt2.defaultValue === void 0) {
2320
+ return `${prompt2.environment} \xB7 optional, Enter leaves it unset`;
2321
+ }
2322
+ const shown = prompt2.defaultValue.length > 24 ? `${prompt2.defaultValue.slice(0, 23)}\u2026` : prompt2.defaultValue;
2323
+ return `${prompt2.environment} \xB7 optional, Enter keeps ${shown}`;
2324
+ }
2057
2325
  var fillCommand = (0, import_citty6.defineCommand)({
2058
2326
  meta: {
2059
2327
  name: "fill",
@@ -2064,8 +2332,8 @@ var fillCommand = (0, import_citty6.defineCommand)({
2064
2332
  },
2065
2333
  run({ args }) {
2066
2334
  return guard(async () => {
2067
- const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
2068
- const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2335
+ const reader = lineReader();
2336
+ const ask = (prompt2) => reader.ask(prompt(prompt2.parameter, contextFor(prompt2)));
2069
2337
  try {
2070
2338
  write(
2071
2339
  renderFill(
@@ -2077,7 +2345,7 @@ var fillCommand = (0, import_citty6.defineCommand)({
2077
2345
  )
2078
2346
  );
2079
2347
  } finally {
2080
- rl.close();
2348
+ reader.close();
2081
2349
  }
2082
2350
  });
2083
2351
  }
@@ -2239,12 +2507,14 @@ async function runExplain(options) {
2239
2507
  function renderExplain(explanation) {
2240
2508
  const target = explanation.location === void 0 ? "nothing" : `${PENV_DIR}/${explanation.location}`;
2241
2509
  const rows = explanation.candidates.map((candidate) => [
2242
- candidate.location,
2243
- candidate.wins ? "present, wins" : candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
2510
+ candidate.wins ? candidate.location : out.dim(candidate.location),
2511
+ candidate.wins ? out.green("present, wins") : out.dim(
2512
+ candidate.present ? candidate.skipped ?? "present" : candidate.skipped ?? "absent"
2513
+ )
2244
2514
  ]);
2245
2515
  return [
2246
- `${explanation.parameter} resolves to ${target} for environment ${explanation.environment}`,
2247
- ...explanation.undecryptable === void 0 ? [] : [` penv cannot decrypt it: ${explanation.undecryptable}`],
2516
+ `${out.bold(explanation.parameter)} resolves to ${target} for environment ${explanation.environment}`,
2517
+ ...explanation.undecryptable === void 0 ? [] : [` ${out.red(CROSS)} penv cannot decrypt it: ${explanation.undecryptable}`],
2248
2518
  "",
2249
2519
  ...columns(rows).map((line) => ` ${line}`)
2250
2520
  ];
@@ -2382,7 +2652,7 @@ function hasImportsBlock(cwd) {
2382
2652
  // src/commands/init.ts
2383
2653
  var import_node_fs3 = require("fs");
2384
2654
  var import_node_path6 = require("path");
2385
- var import_promises2 = require("readline/promises");
2655
+ var import_promises = require("readline/promises");
2386
2656
  var import_core14 = require("@penvhq/core");
2387
2657
  var import_citty9 = require("citty");
2388
2658
  var CONFIG_FILE = "penv.config.ts";
@@ -2523,23 +2793,27 @@ function planInit(root, flags = {}) {
2523
2793
  function renderPlan(plan2) {
2524
2794
  const rows = [];
2525
2795
  rows.push([
2526
- " environments",
2527
- plan2.suggestedEnvironments.length === 0 ? "" : `[${plan2.suggestedEnvironments.join(", ")}]`,
2528
- plan2.suggestedEnvironments.length === 0 ? "<- name them, or Enter to leave the whitelist empty" : "<- from your .env files; edit, or Enter to accept"
2796
+ ` ${out.dim("environments")}`,
2797
+ plan2.suggestedEnvironments.length === 0 ? "" : out.cyan(`[${plan2.suggestedEnvironments.join(", ")}]`),
2798
+ out.dim(
2799
+ plan2.suggestedEnvironments.length === 0 ? "\u2190 name them, or Enter to leave the whitelist empty" : "\u2190 from your .env files; edit, or Enter to accept"
2800
+ )
2529
2801
  ]);
2530
2802
  rows.push([
2531
- " schemaFile",
2803
+ ` ${out.dim("schemaFile")}`,
2532
2804
  plan2.decisions.schemaFile,
2533
- plan2.decisions.schemaFile === import_core14.DEFAULT_SCHEMA_FILE ? "" : `(default: ${import_core14.DEFAULT_SCHEMA_FILE})`
2805
+ plan2.decisions.schemaFile === import_core14.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core14.DEFAULT_SCHEMA_FILE})`)
2534
2806
  ]);
2535
2807
  for (const prefix of plan2.decisions.publicPrefixes) {
2536
- rows.push([" publicPrefix", prefix, ""]);
2808
+ rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
2537
2809
  }
2538
- const headline = plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`;
2810
+ const headline = out.bold(
2811
+ plan2.detected === void 0 ? "No framework detected in package.json." : `Detected ${plan2.detected.name}.`
2812
+ );
2539
2813
  return [headline, "", ...columns(rows), ""];
2540
2814
  }
2541
2815
  function environmentsHint(plan2) {
2542
- return plan2.suggestedEnvironments.length === 0 ? "environments (comma-separated, Enter for none) > " : 'environments (Enter to accept, "none" for an empty whitelist) > ';
2816
+ return plan2.suggestedEnvironments.length === 0 ? prompt("environments", "comma-separated, Enter for none") : prompt("environments", 'Enter to accept, "none" for an empty whitelist');
2543
2817
  }
2544
2818
  async function promptForDecisions(plan2, io) {
2545
2819
  for (const line of renderPlan(plan2)) {
@@ -2554,7 +2828,7 @@ async function promptForDecisions(plan2, io) {
2554
2828
  );
2555
2829
  io.write("");
2556
2830
  }
2557
- const proceed = (await io.ask("Proceed? [Y/n] ")).trim().toLowerCase();
2831
+ const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
2558
2832
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
2559
2833
  return void 0;
2560
2834
  }
@@ -2959,14 +3233,15 @@ function renderInit(result2) {
2959
3233
  return [
2960
3234
  ...formatSteps(steps),
2961
3235
  "",
2962
- `Done. Declare your parameters in ${result2.decisions.schemaFile}, then \`penv set <key>\`.`,
3236
+ `${out.green(CHECK)} ${out.bold("Done.")} Declare your parameters in ${result2.decisions.schemaFile}, then:`,
3237
+ tip(out.cyan("penv set <key>")),
2963
3238
  ...result2.decisions.environments.length === 0 ? [
2964
3239
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2965
3240
  ] : []
2966
3241
  ];
2967
3242
  }
2968
3243
  async function askOnTty(plan2) {
2969
- const rl = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
3244
+ const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
2970
3245
  try {
2971
3246
  return await promptForDecisions(plan2, {
2972
3247
  ask: (question) => rl.question(question),
@@ -3429,20 +3704,28 @@ function runKeyCreate(options) {
3429
3704
  function renderKeyCreate(result2) {
3430
3705
  if (result2.source === "keychain") {
3431
3706
  return [
3432
- `A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
3707
+ `${out.green(CHECK)} A new key for environment ${result2.environment}, stored in your OS keychain as \`${result2.id}\`.`,
3433
3708
  "",
3434
- "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
3435
- "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
3709
+ out.dim(
3710
+ "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running"
3711
+ ),
3712
+ out.dim(
3713
+ "`penv key create` again would replace it \u2014 so it lives in exactly one place, on this machine."
3714
+ )
3436
3715
  ];
3437
3716
  }
3438
3717
  return [
3439
- `A new key for environment ${result2.environment}. penv did not store it.`,
3718
+ `${out.green(CHECK)} A new key for environment ${result2.environment}. penv did not store it.`,
3440
3719
  "",
3441
- ` ${result2.variable}=${result2.key}`,
3720
+ ` ${out.cyan(`${result2.variable}=${result2.key}`)}`,
3442
3721
  "",
3443
- "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
3444
- "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
3445
- "without it, and penv keeps no copy to fall back on."
3722
+ out.dim(
3723
+ "Export it where penv runs, and put it wherever this environment's secrets already live \u2014"
3724
+ ),
3725
+ out.dim(
3726
+ "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable"
3727
+ ),
3728
+ out.dim("without it, and penv keeps no copy to fall back on.")
3446
3729
  ];
3447
3730
  }
3448
3731
  var keyCommand = (0, import_citty11.defineCommand)({
@@ -3510,11 +3793,31 @@ async function runList(options) {
3510
3793
  }
3511
3794
  return { environment, parameters };
3512
3795
  }
3796
+ function paintScope(entry) {
3797
+ if (entry.scope === "absent" || entry.viaUnscopedFallback) {
3798
+ return out.yellow(entry.scope);
3799
+ }
3800
+ return out.green(entry.scope);
3801
+ }
3513
3802
  function renderList(result2) {
3514
3803
  if (result2.parameters.length === 0) {
3515
- return [`No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`];
3804
+ return [
3805
+ `No parameters in ${PENV_DIR}/ for environment ${result2.environment}.`,
3806
+ tip(`penv set <key> --env ${result2.environment}`)
3807
+ ];
3516
3808
  }
3517
- return columns(result2.parameters.map((entry) => [entry.parameter, entry.scope, entry.variable]));
3809
+ const header = ["parameter", "scope", "variable", ""].map((cell) => out.dim(cell));
3810
+ const rows = result2.parameters.map((entry) => [
3811
+ entry.parameter,
3812
+ paintScope(entry),
3813
+ entry.variable,
3814
+ entry.encrypted ? out.dim("encrypted") : ""
3815
+ ]);
3816
+ return [
3817
+ heading("penv list", `environment ${result2.environment}`),
3818
+ "",
3819
+ ...columns([header, ...rows])
3820
+ ];
3518
3821
  }
3519
3822
  var listCommand = (0, import_citty12.defineCommand)({
3520
3823
  meta: { name: "list", description: "List parameters" },
@@ -3654,8 +3957,9 @@ function renderMove(result2) {
3654
3957
  const lines = formatRows(rows);
3655
3958
  lines.push(
3656
3959
  "",
3657
- ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3658
- " or `penv validate` will report the value as unused and the declaration as unset."
3960
+ tip(
3961
+ `.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.`
3962
+ )
3659
3963
  );
3660
3964
  return lines;
3661
3965
  }
@@ -4184,9 +4488,9 @@ function renderDrift(drift, environment) {
4184
4488
  if (rows.length === 0) {
4185
4489
  return [];
4186
4490
  }
4187
- const lines = ["", `Schema and tree differ for ${environment}:`, ...formatRows(rows)];
4491
+ const lines = ["", out.bold(`Schema and tree differ for ${environment}:`), ...formatRows(rows)];
4188
4492
  for (const remedy of new Set(drift.declared.map((item) => item.remedy))) {
4189
- lines.push(` ${remedy}`);
4493
+ lines.push(tip(remedy));
4190
4494
  }
4191
4495
  return lines;
4192
4496
  }
@@ -4217,7 +4521,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
4217
4521
  process.exitCode = previous;
4218
4522
  }
4219
4523
  });
4220
- write(["Watching .penv/ and penv.config.ts. Ctrl-C to stop."]);
4524
+ write([`Watching .penv/ and penv.config.ts. ${out.dim("Ctrl-C to stop.")}`]);
4221
4525
  await new Promise((resolve7) => {
4222
4526
  process.once("SIGINT", () => {
4223
4527
  handle.close();