@penvhq/cli 0.3.2 → 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}
515
642
  `);
643
+ if (error.remedy !== void 0) {
644
+ process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
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}
518
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)}
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;
@@ -914,18 +1051,26 @@ function renderValidate(result2) {
914
1051
  ]);
915
1052
  }
916
1053
  const rows = result2.issues.map((issue) => ({
917
- glyph: WARN,
1054
+ glyph: CROSS,
918
1055
  label: LABELS[issue.kind],
919
1056
  subject: issue.subject,
920
1057
  detail: issue.message
921
1058
  }));
922
1059
  const lines = formatRows(rows);
923
- const remedies = [...new Set(result2.issues.map((issue) => issue.remedy))];
924
- for (const remedy of remedies) {
925
- if (remedy !== void 0) {
926
- 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));
927
1067
  }
928
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
+ );
929
1074
  return lines;
930
1075
  }
931
1076
  var validateCommand = (0, import_citty2.defineCommand)({
@@ -1680,22 +1825,51 @@ async function providerDriftFindings(project, environment, override) {
1680
1825
  }
1681
1826
  ];
1682
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
+ }
1683
1852
  function renderDoctor(report) {
1684
1853
  const rows = report.findings.map((finding) => ({
1685
- glyph: finding.severity === "pass" ? CHECK : finding.severity === "unknown" ? UNKNOWN : WARN,
1854
+ glyph: GLYPHS[finding.severity],
1686
1855
  label: finding.label,
1687
1856
  ...finding.subject === void 0 ? {} : { subject: finding.subject },
1688
1857
  ...finding.detail === void 0 ? {} : { detail: finding.detail }
1689
1858
  }));
1690
- const lines = formatRows(rows);
1859
+ const lines = [heading("penv doctor", `environment ${report.environment}`), ""];
1860
+ lines.push(...formatRows(rows));
1691
1861
  const remedies = [
1692
1862
  ...new Set(
1693
1863
  report.findings.filter((finding) => finding.severity !== "pass").map((finding) => finding.remedy).filter((remedy) => remedy !== void 0)
1694
1864
  )
1695
1865
  ];
1696
- for (const remedy of remedies) {
1697
- lines.push(` ${remedy}`);
1866
+ if (remedies.length > 0) {
1867
+ lines.push("");
1868
+ for (const remedy of remedies) {
1869
+ lines.push(tip(remedy));
1870
+ }
1698
1871
  }
1872
+ lines.push("", summarize(report));
1699
1873
  return lines;
1700
1874
  }
1701
1875
  var doctorCommand = (0, import_citty3.defineCommand)({
@@ -1986,9 +2160,52 @@ var decryptCommand = (0, import_citty5.defineCommand)({
1986
2160
  });
1987
2161
 
1988
2162
  // src/commands/fill.ts
1989
- var import_promises = require("readline/promises");
1990
2163
  var import_core10 = require("@penvhq/core");
1991
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
1992
2209
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
1993
2210
  async function runFill(options) {
1994
2211
  const validation = await runValidate({
@@ -2010,6 +2227,7 @@ ${detail}`,
2010
2227
  }
2011
2228
  const written = [];
2012
2229
  const skipped2 = [];
2230
+ const kept = [];
2013
2231
  const unreachable = [];
2014
2232
  for (const drift of validation.drift.declared) {
2015
2233
  if (drift.ref === void 0) {
@@ -2018,7 +2236,12 @@ ${detail}`,
2018
2236
  }
2019
2237
  const ref = drift.ref;
2020
2238
  const key = [...ref.namespace, ref.name].join("/");
2021
- 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
+ });
2022
2245
  if (value === void 0 || value === "") {
2023
2246
  skipped2.push(drift.subject);
2024
2247
  continue;
@@ -2030,17 +2253,45 @@ ${detail}`,
2030
2253
  encrypted: result2.encrypted
2031
2254
  });
2032
2255
  }
2033
- 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 };
2034
2282
  }
2035
2283
  function summaryLine(result2) {
2036
2284
  const filled = result2.written.length;
2037
- if (filled === 0 && result2.skipped.length === 0 && result2.unreachable.length === 0) {
2038
- 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`;
2039
2287
  }
2040
2288
  const parts = [`${filled} written`];
2041
2289
  if (result2.skipped.length > 0) {
2042
2290
  parts.push(`${result2.skipped.length} skipped`);
2043
2291
  }
2292
+ if (result2.kept.length > 0) {
2293
+ parts.push(`${result2.kept.length} left to the schema's defaults`);
2294
+ }
2044
2295
  if (result2.unreachable.length > 0) {
2045
2296
  parts.push(`${result2.unreachable.length} unreachable`);
2046
2297
  }
@@ -2061,6 +2312,16 @@ function renderFill(result2) {
2061
2312
  lines.push(summaryLine(result2));
2062
2313
  return lines;
2063
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
+ }
2064
2325
  var fillCommand = (0, import_citty6.defineCommand)({
2065
2326
  meta: {
2066
2327
  name: "fill",
@@ -2071,8 +2332,8 @@ var fillCommand = (0, import_citty6.defineCommand)({
2071
2332
  },
2072
2333
  run({ args }) {
2073
2334
  return guard(async () => {
2074
- const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
2075
- const ask = (prompt) => rl.question(`${prompt.parameter} (${prompt.environment}): `);
2335
+ const reader = lineReader();
2336
+ const ask = (prompt2) => reader.ask(prompt(prompt2.parameter, contextFor(prompt2)));
2076
2337
  try {
2077
2338
  write(
2078
2339
  renderFill(
@@ -2084,7 +2345,7 @@ var fillCommand = (0, import_citty6.defineCommand)({
2084
2345
  )
2085
2346
  );
2086
2347
  } finally {
2087
- rl.close();
2348
+ reader.close();
2088
2349
  }
2089
2350
  });
2090
2351
  }
@@ -2246,12 +2507,14 @@ async function runExplain(options) {
2246
2507
  function renderExplain(explanation) {
2247
2508
  const target = explanation.location === void 0 ? "nothing" : `${PENV_DIR}/${explanation.location}`;
2248
2509
  const rows = explanation.candidates.map((candidate) => [
2249
- candidate.location,
2250
- 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
+ )
2251
2514
  ]);
2252
2515
  return [
2253
- `${explanation.parameter} resolves to ${target} for environment ${explanation.environment}`,
2254
- ...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}`],
2255
2518
  "",
2256
2519
  ...columns(rows).map((line) => ` ${line}`)
2257
2520
  ];
@@ -2389,7 +2652,7 @@ function hasImportsBlock(cwd) {
2389
2652
  // src/commands/init.ts
2390
2653
  var import_node_fs3 = require("fs");
2391
2654
  var import_node_path6 = require("path");
2392
- var import_promises2 = require("readline/promises");
2655
+ var import_promises = require("readline/promises");
2393
2656
  var import_core14 = require("@penvhq/core");
2394
2657
  var import_citty9 = require("citty");
2395
2658
  var CONFIG_FILE = "penv.config.ts";
@@ -2530,23 +2793,27 @@ function planInit(root, flags = {}) {
2530
2793
  function renderPlan(plan2) {
2531
2794
  const rows = [];
2532
2795
  rows.push([
2533
- " environments",
2534
- plan2.suggestedEnvironments.length === 0 ? "" : `[${plan2.suggestedEnvironments.join(", ")}]`,
2535
- 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
+ )
2536
2801
  ]);
2537
2802
  rows.push([
2538
- " schemaFile",
2803
+ ` ${out.dim("schemaFile")}`,
2539
2804
  plan2.decisions.schemaFile,
2540
- 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})`)
2541
2806
  ]);
2542
2807
  for (const prefix of plan2.decisions.publicPrefixes) {
2543
- rows.push([" publicPrefix", prefix, ""]);
2808
+ rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
2544
2809
  }
2545
- 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
+ );
2546
2813
  return [headline, "", ...columns(rows), ""];
2547
2814
  }
2548
2815
  function environmentsHint(plan2) {
2549
- 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');
2550
2817
  }
2551
2818
  async function promptForDecisions(plan2, io) {
2552
2819
  for (const line of renderPlan(plan2)) {
@@ -2561,7 +2828,7 @@ async function promptForDecisions(plan2, io) {
2561
2828
  );
2562
2829
  io.write("");
2563
2830
  }
2564
- const proceed = (await io.ask("Proceed? [Y/n] ")).trim().toLowerCase();
2831
+ const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
2565
2832
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
2566
2833
  return void 0;
2567
2834
  }
@@ -2966,14 +3233,15 @@ function renderInit(result2) {
2966
3233
  return [
2967
3234
  ...formatSteps(steps),
2968
3235
  "",
2969
- `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>")),
2970
3238
  ...result2.decisions.environments.length === 0 ? [
2971
3239
  `Then declare your environments in ${CONFIG_FILE}: penv leaves the whitelist empty rather than inventing one, and every command needs it.`
2972
3240
  ] : []
2973
3241
  ];
2974
3242
  }
2975
3243
  async function askOnTty(plan2) {
2976
- 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 });
2977
3245
  try {
2978
3246
  return await promptForDecisions(plan2, {
2979
3247
  ask: (question) => rl.question(question),
@@ -3436,20 +3704,28 @@ function runKeyCreate(options) {
3436
3704
  function renderKeyCreate(result2) {
3437
3705
  if (result2.source === "keychain") {
3438
3706
  return [
3439
- `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}\`.`,
3440
3708
  "",
3441
- "penv kept no copy. Anything sealed under it is unreadable without your keychain, and running",
3442
- "`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
+ )
3443
3715
  ];
3444
3716
  }
3445
3717
  return [
3446
- `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.`,
3447
3719
  "",
3448
- ` ${result2.variable}=${result2.key}`,
3720
+ ` ${out.cyan(`${result2.variable}=${result2.key}`)}`,
3449
3721
  "",
3450
- "Export it where penv runs, and put it wherever this environment's secrets already live \u2014",
3451
- "a KMS, your CI's secret store, a password manager. Anything sealed under it is unreadable",
3452
- "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.")
3453
3729
  ];
3454
3730
  }
3455
3731
  var keyCommand = (0, import_citty11.defineCommand)({
@@ -3517,11 +3793,31 @@ async function runList(options) {
3517
3793
  }
3518
3794
  return { environment, parameters };
3519
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
+ }
3520
3802
  function renderList(result2) {
3521
3803
  if (result2.parameters.length === 0) {
3522
- 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
+ ];
3523
3808
  }
3524
- 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
+ ];
3525
3821
  }
3526
3822
  var listCommand = (0, import_citty12.defineCommand)({
3527
3823
  meta: { name: "list", description: "List parameters" },
@@ -3661,8 +3957,9 @@ function renderMove(result2) {
3661
3957
  const lines = formatRows(rows);
3662
3958
  lines.push(
3663
3959
  "",
3664
- ` .penv/env.ts still declares \`${result2.schema.was}\`. Rename it to \`${result2.schema.now}\`,`,
3665
- " 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
+ )
3666
3963
  );
3667
3964
  return lines;
3668
3965
  }
@@ -4191,9 +4488,9 @@ function renderDrift(drift, environment) {
4191
4488
  if (rows.length === 0) {
4192
4489
  return [];
4193
4490
  }
4194
- const lines = ["", `Schema and tree differ for ${environment}:`, ...formatRows(rows)];
4491
+ const lines = ["", out.bold(`Schema and tree differ for ${environment}:`), ...formatRows(rows)];
4195
4492
  for (const remedy of new Set(drift.declared.map((item) => item.remedy))) {
4196
- lines.push(` ${remedy}`);
4493
+ lines.push(tip(remedy));
4197
4494
  }
4198
4495
  return lines;
4199
4496
  }
@@ -4224,7 +4521,7 @@ var watchCommand = (0, import_citty17.defineCommand)({
4224
4521
  process.exitCode = previous;
4225
4522
  }
4226
4523
  });
4227
- 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.")}`]);
4228
4525
  await new Promise((resolve7) => {
4229
4526
  process.once("SIGINT", () => {
4230
4527
  handle.close();