@super-repo/envx 0.2.3-b.5 → 0.2.3-b.6

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/auto.js CHANGED
@@ -1,4 +1,4 @@
1
- import envx from "./index.js";
1
+ import { t as envx } from "./chunks/src-B_rA7_sV.js";
2
2
  //#region src/auto.ts
3
3
  envx();
4
4
  //#endregion
@@ -1,8 +1,8 @@
1
- import { A as isEncrypted, C as parseEnv, D as decryptValueAsymmetric, S as log, T as toRecord, _ as resolveCwdOrWorkspace, a as auditFiles, b as expandEnvSrc, c as encryptFiles, f as DEFAULT_NODE_ENV_MAP, g as loadEnv, h as listEnvFiles, l as defaultKeysPath, m as findWorkspaceRoot, o as rotateFiles, p as detectEnvironment, r as loadDotenvxConfig, s as decryptFiles, t as writeProcessed, u as readKeysFile, v as resolveEnvPaths, x as expandRecord, y as validateCmdVariable } from "./src-jaqb5pGP.js";
1
+ import { A as isEncrypted, C as parseEnv, D as decryptValueAsymmetric, S as log, T as toRecord, _ as resolveCwdOrWorkspace, a as auditFiles, b as expandEnvSrc, c as encryptFiles, f as DEFAULT_NODE_ENV_MAP, g as loadEnv, h as listEnvFiles, l as defaultKeysPath, m as findWorkspaceRoot, o as rotateFiles, p as detectEnvironment, r as loadDotenvxConfig, s as decryptFiles, t as writeProcessed, u as readKeysFile, v as resolveEnvPaths, x as expandRecord, y as validateCmdVariable } from "./src-BeMTu_ms.js";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import yargs from "yargs";
5
- import { execSync, spawn } from "child_process";
5
+ import { execFileSync, execSync, spawn } from "child_process";
6
6
  //#region src/commands/audit.ts
7
7
  /**
8
8
  * Walk the repo looking for plaintext secrets — committed AWS keys,
@@ -30,6 +30,14 @@ var auditCommand = {
30
30
  type: "number",
31
31
  default: 50,
32
32
  describe: "Stop after reporting this many findings (0 = unlimited)."
33
+ }).option("respect-gitignore", {
34
+ type: "boolean",
35
+ default: true,
36
+ describe: "Skip files and directories matched by .gitignore (and .git/info/exclude). Pass --no-respect-gitignore to scan everything."
37
+ }).option("staged", {
38
+ type: "boolean",
39
+ default: false,
40
+ describe: "Scan only files staged for the next commit (added/copied/modified/renamed). Mutually exclusive with positional paths. Suitable as the body of a pre-commit hook."
33
41
  }).option("json", {
34
42
  type: "boolean",
35
43
  default: false,
@@ -37,14 +45,37 @@ var auditCommand = {
37
45
  }),
38
46
  handler: (argv) => {
39
47
  const rawPaths = argv["paths"];
40
- const roots = (rawPaths && rawPaths.length > 0 ? rawPaths : ["."]).map((p) => path.resolve(process.cwd(), p));
48
+ const staged = argv["staged"];
41
49
  const ignore = argv["ignore"];
42
50
  const max = argv["max"];
51
+ const respectGitignore = argv["respect-gitignore"];
43
52
  const asJson = argv["json"];
53
+ let roots;
54
+ if (staged) {
55
+ if (rawPaths && rawPaths.length > 0) {
56
+ log.error("audit: --staged cannot be combined with positional paths (it scans the git-staged set)");
57
+ process.exit(2);
58
+ }
59
+ const stagedFiles = listStagedFiles();
60
+ if (stagedFiles === null) {
61
+ log.error("audit: --staged requires a git repository (no .git directory found above cwd)");
62
+ process.exit(2);
63
+ }
64
+ if (stagedFiles.length === 0) {
65
+ if (asJson) process.stdout.write(JSON.stringify({
66
+ filesScanned: 0,
67
+ findings: []
68
+ }) + "\n");
69
+ else log.info("audit: no staged files — nothing to scan");
70
+ return;
71
+ }
72
+ roots = stagedFiles.map((p) => path.resolve(process.cwd(), p));
73
+ } else roots = (rawPaths && rawPaths.length > 0 ? rawPaths : ["."]).map((p) => path.resolve(process.cwd(), p));
44
74
  const { findings, filesScanned } = auditFiles({
45
75
  roots,
46
76
  ignore,
47
- max
77
+ max,
78
+ respectGitignore
48
79
  });
49
80
  if (asJson) {
50
81
  process.stdout.write(JSON.stringify({
@@ -86,6 +117,36 @@ function groupByPattern(findings) {
86
117
  }
87
118
  return [...map.entries()];
88
119
  }
120
+ /**
121
+ * Returns the list of files staged for the next commit (added, copied,
122
+ * modified, or renamed — `--diff-filter=ACMR` excludes deletions). NUL-
123
+ * delimited (`-z`) so paths with spaces or newlines round-trip safely.
124
+ *
125
+ * Returns `null` when not inside a git repo (so the caller can render a
126
+ * nicer error than git's stderr). Empty array means "in a repo, but
127
+ * nothing staged" — distinct from the failure case.
128
+ *
129
+ * Exported for unit testing.
130
+ */
131
+ function listStagedFiles() {
132
+ try {
133
+ const out = execFileSync("git", [
134
+ "diff",
135
+ "--cached",
136
+ "--name-only",
137
+ "--diff-filter=ACMR",
138
+ "-z"
139
+ ], { stdio: [
140
+ "ignore",
141
+ "pipe",
142
+ "pipe"
143
+ ] }).toString("utf8");
144
+ if (out.length === 0) return [];
145
+ return (out.endsWith("\0") ? out.slice(0, -1) : out).split("\0").filter((p) => p.length > 0);
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
89
150
  //#endregion
90
151
  //#region src/commands/bake.ts
91
152
  /**
@@ -1062,11 +1123,190 @@ var runCommand = {
1062
1123
  }
1063
1124
  };
1064
1125
  //#endregion
1126
+ //#region src/commands/template-ai.ts
1127
+ var ANTHROPIC_DEFAULT_BASE = "https://api.anthropic.com";
1128
+ var OPENAI_DEFAULT_BASE = "https://api.openai.com/v1";
1129
+ var ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
1130
+ var ANTHROPIC_OAUTH_ENVS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN"];
1131
+ var REQUEST_TIMEOUT_MS = 6e4;
1132
+ var AiError = class extends Error {
1133
+ constructor(message) {
1134
+ super(`envx template ai: ${message}`);
1135
+ this.name = "AiError";
1136
+ }
1137
+ };
1138
+ /**
1139
+ * Resolve an API key from `process.env`. Looks at `apiKeyEnv` first
1140
+ * (defaults to provider-appropriate name), then known OAuth fallbacks
1141
+ * for Anthropic. Returns `null` when nothing is set — caller decides
1142
+ * how to surface the failure.
1143
+ */
1144
+ function resolveAuth(provider, apiKeyEnv) {
1145
+ const direct = process.env[apiKeyEnv];
1146
+ if (direct) return {
1147
+ token: direct,
1148
+ kind: provider === "anthropic" && direct.startsWith("sk-ant-oat") ? "oauth" : "api-key",
1149
+ sourceEnv: apiKeyEnv
1150
+ };
1151
+ if (provider === "anthropic") for (const name of ANTHROPIC_OAUTH_ENVS) {
1152
+ const v = process.env[name];
1153
+ if (v) return {
1154
+ token: v,
1155
+ kind: v.startsWith("sk-ant-oat") ? "oauth" : "api-key",
1156
+ sourceEnv: name
1157
+ };
1158
+ }
1159
+ return null;
1160
+ }
1161
+ /**
1162
+ * Generate an example value per key. Returns a Map keyed by env-var
1163
+ * name. Keys without a usable AI response fall back to an empty string
1164
+ * so the template still serializes cleanly.
1165
+ *
1166
+ * `context` is an optional per-key hint the renderer can supply (e.g.
1167
+ * the trailing comment from the source line, or the comment block
1168
+ * directly above) to anchor the generation.
1169
+ */
1170
+ async function generateExamples(keys, opts = {}) {
1171
+ if (keys.length === 0) return /* @__PURE__ */ new Map();
1172
+ const provider = opts.provider ?? "anthropic";
1173
+ const apiKeyEnv = opts.apiKeyEnv ?? (provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
1174
+ const auth = resolveAuth(provider, apiKeyEnv);
1175
+ if (!auth) throw new AiError(`${apiKeyEnv} not set in process.env (load it from your .env, or export it). For Anthropic, ${ANTHROPIC_OAUTH_ENVS.join(" / ")} are also accepted.`);
1176
+ const model = opts.model ?? (provider === "anthropic" ? "claude-haiku-4-5-20251001" : "gpt-4o-mini");
1177
+ const prompt = buildPrompt(keys);
1178
+ return parseResponse(provider === "anthropic" ? await callAnthropic({
1179
+ auth,
1180
+ model,
1181
+ prompt,
1182
+ baseUrl: opts.baseUrl
1183
+ }) : await callOpenAi({
1184
+ apiKey: auth.token,
1185
+ model,
1186
+ prompt,
1187
+ baseUrl: opts.baseUrl
1188
+ }), keys.map((k) => k.name));
1189
+ }
1190
+ function buildPrompt(keys) {
1191
+ return [
1192
+ "You are generating placeholder values for a `.env.example` file.",
1193
+ "Each value should be a realistic-looking but obviously-fake example a developer can copy and replace.",
1194
+ "",
1195
+ "Rules:",
1196
+ "- Output JSON only, no prose, no code fences. A single object mapping each key (verbatim) to its example string value.",
1197
+ "- Use shape that matches the key name: URLs for *_URL, ports for PORT/*_PORT, booleans (`true`/`false`) for IS_*/HAS_*/ENABLE_*, hex/base64-ish strings for *_KEY/*_TOKEN/*_SECRET, integers for *_COUNT/*_LIMIT, log levels for LOG_LEVEL, etc.",
1198
+ "- Never use a real-looking secret. Prefix sensitive-looking values with `your-` or end with `-here` so reviewers see them as placeholders.",
1199
+ "- Keep each value short — under 60 chars unless a JWT or URL forces it.",
1200
+ "- If a hint is given in parentheses after a key, use it as context.",
1201
+ "",
1202
+ "Keys:",
1203
+ keys.map((k) => k.hint && k.hint.length > 0 ? `- ${k.name} (${k.hint.trim()})` : `- ${k.name}`).join("\n"),
1204
+ "",
1205
+ "Return JSON object:"
1206
+ ].join("\n");
1207
+ }
1208
+ /**
1209
+ * Parse the AI's JSON response. Tolerant of code fences, leading prose,
1210
+ * trailing junk — extracts the first {…} block and JSON.parses it. Any
1211
+ * key missing from the parsed object is omitted from the result map
1212
+ * (caller falls back to an empty value).
1213
+ */
1214
+ function parseResponse(text, expectedKeys) {
1215
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
1216
+ const candidate = fenced ? fenced[1] : text;
1217
+ const start = candidate.indexOf("{");
1218
+ const end = candidate.lastIndexOf("}");
1219
+ if (start === -1 || end === -1 || end <= start) throw new AiError(`AI returned no JSON object: ${text.slice(0, 200)}`);
1220
+ const slice = candidate.slice(start, end + 1);
1221
+ let parsed;
1222
+ try {
1223
+ parsed = JSON.parse(slice);
1224
+ } catch (e) {
1225
+ throw new AiError(`AI returned invalid JSON: ${e.message} — ${slice.slice(0, 200)}`);
1226
+ }
1227
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiError("AI returned a non-object payload");
1228
+ const out = /* @__PURE__ */ new Map();
1229
+ const obj = parsed;
1230
+ for (const k of expectedKeys) {
1231
+ const v = obj[k];
1232
+ if (typeof v === "string") out.set(k, v);
1233
+ else if (typeof v === "number" || typeof v === "boolean") out.set(k, String(v));
1234
+ }
1235
+ return out;
1236
+ }
1237
+ async function fetchWithTimeout(url, init, ms) {
1238
+ const ctrl = new AbortController();
1239
+ const timer = setTimeout(() => ctrl.abort(), ms);
1240
+ try {
1241
+ return await fetch(url, {
1242
+ ...init,
1243
+ signal: ctrl.signal
1244
+ });
1245
+ } catch (err) {
1246
+ if (err.name === "AbortError") throw new AiError(`request timed out after ${ms}ms (${url})`);
1247
+ throw err;
1248
+ } finally {
1249
+ clearTimeout(timer);
1250
+ }
1251
+ }
1252
+ async function callAnthropic(opts) {
1253
+ const base = opts.baseUrl ?? ANTHROPIC_DEFAULT_BASE;
1254
+ const headers = {
1255
+ "content-type": "application/json",
1256
+ "anthropic-version": "2023-06-01"
1257
+ };
1258
+ if (opts.auth.kind === "oauth") {
1259
+ headers.authorization = `Bearer ${opts.auth.token}`;
1260
+ headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
1261
+ } else headers["x-api-key"] = opts.auth.token;
1262
+ const res = await fetchWithTimeout(`${base}/v1/messages`, {
1263
+ method: "POST",
1264
+ headers,
1265
+ body: JSON.stringify({
1266
+ model: opts.model,
1267
+ max_tokens: 2048,
1268
+ messages: [{
1269
+ role: "user",
1270
+ content: opts.prompt
1271
+ }]
1272
+ })
1273
+ }, REQUEST_TIMEOUT_MS);
1274
+ if (!res.ok) throw new AiError(`anthropic ${res.status}: ${await res.text()}`);
1275
+ const block = (await res.json()).content?.find((b) => b.type === "text");
1276
+ if (!block?.text) throw new AiError("anthropic returned no text content");
1277
+ return block.text;
1278
+ }
1279
+ async function callOpenAi(opts) {
1280
+ const res = await fetchWithTimeout(`${opts.baseUrl ?? OPENAI_DEFAULT_BASE}/chat/completions`, {
1281
+ method: "POST",
1282
+ headers: {
1283
+ "content-type": "application/json",
1284
+ authorization: `Bearer ${opts.apiKey}`
1285
+ },
1286
+ body: JSON.stringify({
1287
+ model: opts.model,
1288
+ messages: [{
1289
+ role: "user",
1290
+ content: opts.prompt
1291
+ }],
1292
+ temperature: .2
1293
+ })
1294
+ }, REQUEST_TIMEOUT_MS);
1295
+ if (!res.ok) throw new AiError(`openai ${res.status}: ${await res.text()}`);
1296
+ const text = (await res.json()).choices?.[0]?.message?.content;
1297
+ if (!text) throw new AiError("openai returned no text content");
1298
+ return text;
1299
+ }
1300
+ //#endregion
1065
1301
  //#region src/commands/template.ts
1066
1302
  /**
1067
1303
  * Generate (or check) a stripped `.env.example` from one or more real
1068
- * env files. Values are removed; keys, declaration order, comments,
1069
- * and blank lines are preserved so the example file stays readable.
1304
+ * env files. The output mirrors the source files' organization —
1305
+ * comments, blank lines, and declaration order are preserved verbatim.
1306
+ * Values are stripped (or replaced with AI-generated placeholders when
1307
+ * `--ai` is set). The encryption banner block (`#/-----[ENVX_PUBLIC_KEY]…`)
1308
+ * and the `*PUBLIC_KEY*` kv lines themselves are dropped — the example
1309
+ * file should be encryption-agnostic.
1070
1310
  *
1071
1311
  * Three modes:
1072
1312
  * - default: write the rendered template to --out (default `.env.example`)
@@ -1087,17 +1327,27 @@ var templateCommand = {
1087
1327
  type: "boolean",
1088
1328
  default: false,
1089
1329
  describe: "Compare the rendered template against the on-disk file; exit 1 on drift. Use this in CI."
1090
- }).option("annotate", {
1330
+ }).option("ai", {
1091
1331
  type: "boolean",
1092
- default: true,
1093
- describe: "Prefix each KEY= with a `# from <source-file>` comment so reviewers can see where each key came from."
1332
+ default: false,
1333
+ describe: "Use Claude (or OpenAI) to generate realistic-looking placeholder values for each key. Reads ANTHROPIC_API_KEY (or OPENAI_API_KEY) from your loaded env files. Non-deterministic usually paired with manual review, not --check."
1334
+ }).option("ai-provider", {
1335
+ type: "string",
1336
+ choices: ["anthropic", "openai"],
1337
+ default: "anthropic",
1338
+ describe: "Which AI provider --ai uses. Default: anthropic."
1339
+ }).option("ai-model", {
1340
+ type: "string",
1341
+ describe: "Override the AI model. Defaults: claude-haiku-4-5-20251001 (anthropic), gpt-4o-mini (openai)."
1094
1342
  }),
1095
- handler: (argv) => {
1343
+ handler: async (argv) => {
1096
1344
  const cfg = argv["__envxConfig"] ?? {};
1097
1345
  const outArg = argv["out"];
1098
1346
  const useStdout = argv["stdout"];
1099
1347
  const check = argv["check"];
1100
- const annotate = argv["annotate"];
1348
+ const useAi = argv["ai"];
1349
+ const aiProvider = argv["ai-provider"];
1350
+ const aiModel = argv["ai-model"];
1101
1351
  const paths = resolveEnvPaths({
1102
1352
  ...argv["env"] !== void 0 ? { envFiles: argv["env"] } : {},
1103
1353
  ...argv["cascade"] !== void 0 ? { cascade: argv["cascade"] } : {},
@@ -1110,7 +1360,32 @@ var templateCommand = {
1110
1360
  log.error("no env files found to template from");
1111
1361
  process.exit(1);
1112
1362
  }
1113
- const rendered = renderTemplate(existing, { annotate });
1363
+ let aiValues = /* @__PURE__ */ new Map();
1364
+ if (useAi) {
1365
+ try {
1366
+ loadEnv({
1367
+ envFiles: existing,
1368
+ quiet: true
1369
+ });
1370
+ } catch (e) {
1371
+ log.error(`--ai: failed to load env for API-key lookup: ${e.message}`);
1372
+ process.exit(1);
1373
+ }
1374
+ const keys = collectKeys(existing);
1375
+ if (keys.length === 0) log.warn("--ai: no keys to generate examples for");
1376
+ else try {
1377
+ aiValues = await generateExamples(keys, {
1378
+ provider: aiProvider,
1379
+ ...aiModel !== void 0 ? { model: aiModel } : {}
1380
+ });
1381
+ log.info(`--ai: generated ${aiValues.size}/${keys.length} example value(s)`);
1382
+ } catch (e) {
1383
+ if (e instanceof AiError) log.error(e.message);
1384
+ else log.error(`--ai: ${e.message}`);
1385
+ process.exit(1);
1386
+ }
1387
+ }
1388
+ const rendered = renderTemplate(existing, { aiValues });
1114
1389
  if (useStdout) {
1115
1390
  process.stdout.write(rendered);
1116
1391
  return;
@@ -1129,32 +1404,100 @@ var templateCommand = {
1129
1404
  log.success(`wrote template: ${outPath} (${countKeys(rendered)} key(s))`);
1130
1405
  }
1131
1406
  };
1407
+ /**
1408
+ * Render the template by walking each source file in order. Comments,
1409
+ * blank lines, and declaration order are preserved verbatim. A kv line
1410
+ * becomes `KEY=` (or `KEY=<ai-value>` when `--ai` populated `aiValues`).
1411
+ *
1412
+ * Two filters apply to every line, in this order:
1413
+ * 1. The encryption banner block — any line starting with `#/` (e.g.
1414
+ * `#/-------------------[ENVX_PUBLIC_KEY]------------------/`).
1415
+ * The full 4-line banner is dropped.
1416
+ * 2. Public-key kv lines — `ENVX_PUBLIC_KEY*` and the legacy
1417
+ * `DOTENV_PUBLIC_KEY*`. They're committed alongside encrypted
1418
+ * values, but `.env.example` is meant to be encryption-agnostic.
1419
+ *
1420
+ * Multi-file dedup: when a key appears in more than one source file,
1421
+ * only the first occurrence's line is emitted. Subsequent files'
1422
+ * comments and blank lines still pass through (so the per-file
1423
+ * structure stays visible), but their kv duplicates are skipped.
1424
+ */
1132
1425
  function renderTemplate(files, opts) {
1133
1426
  const seen = /* @__PURE__ */ new Set();
1134
1427
  const out = [];
1135
1428
  out.push("# Generated by `envx template` — do not edit by hand.");
1136
1429
  out.push("# Run `envx template` to regenerate, or `envx template --check` in CI.");
1137
1430
  out.push("");
1138
- for (const file of files) {
1431
+ for (let i = 0; i < files.length; i += 1) {
1432
+ const file = files[i];
1139
1433
  const display = path.relative(process.cwd(), file) || file;
1140
1434
  const lines = parseEnv(fs.readFileSync(file, "utf8"));
1141
- const localKeys = [];
1435
+ if (files.length > 1) out.push(`# ── ${display} ─────────────────────────────────`);
1436
+ appendFile(out, lines, seen, opts.aiValues);
1437
+ if (i < files.length - 1 && (out.length === 0 || out[out.length - 1] !== "")) out.push("");
1438
+ }
1439
+ return out.join("\n");
1440
+ }
1441
+ function appendFile(out, lines, seen, aiValues) {
1442
+ for (const ln of lines) {
1443
+ if (ln.type === "raw") {
1444
+ if (isBannerLine(ln.raw)) continue;
1445
+ out.push(ln.raw);
1446
+ continue;
1447
+ }
1448
+ if (isPublicKeyName(ln.key)) continue;
1449
+ if (seen.has(ln.key)) continue;
1450
+ seen.add(ln.key);
1451
+ const aiValue = aiValues.get(ln.key);
1452
+ if (aiValue !== void 0 && aiValue !== "") {
1453
+ const quoted = /[\s#'"\\$&;|<>(){}]/.test(aiValue) ? `"${aiValue.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : aiValue;
1454
+ out.push(`${ln.key}=${quoted}`);
1455
+ } else out.push(`${ln.key}=`);
1456
+ }
1457
+ }
1458
+ /**
1459
+ * Banner lines emitted by `envx encrypt` start with `#/`. Be permissive:
1460
+ * any comment beginning with that two-char prefix is treated as banner
1461
+ * decoration. Real user comments use `#` (with or without a space).
1462
+ */
1463
+ function isBannerLine(raw) {
1464
+ return /^\s*#\//.test(raw);
1465
+ }
1466
+ /**
1467
+ * `ENVX_PUBLIC_KEY*` is the canonical public-key var; `DOTENV_PUBLIC_KEY*`
1468
+ * is the upstream-dotenvx-compat variant envx also reads. Both are an
1469
+ * artifact of encryption, never user-meaningful — drop from `.env.example`.
1470
+ */
1471
+ function isPublicKeyName(name) {
1472
+ return /^(ENVX|DOTENV)_PUBLIC_KEY/.test(name);
1473
+ }
1474
+ /**
1475
+ * Collect every unique kv key (in declaration order) across the source
1476
+ * files, skipping the encryption-banner public-key vars. Used to feed
1477
+ * `--ai` exactly the keys that will appear in the rendered template.
1478
+ *
1479
+ * Each entry includes a `hint` derived from the kv line's trailing
1480
+ * comment (e.g. `# database connection string`) — the AI prompt uses
1481
+ * the hint to anchor the example value.
1482
+ */
1483
+ function collectKeys(files) {
1484
+ const seen = /* @__PURE__ */ new Set();
1485
+ const out = [];
1486
+ for (const file of files) {
1487
+ const lines = parseEnv(fs.readFileSync(file, "utf8"));
1142
1488
  for (const ln of lines) {
1143
1489
  if (ln.type !== "kv") continue;
1490
+ if (isPublicKeyName(ln.key)) continue;
1144
1491
  if (seen.has(ln.key)) continue;
1145
1492
  seen.add(ln.key);
1146
- localKeys.push(ln);
1147
- }
1148
- if (localKeys.length === 0) continue;
1149
- out.push(`# ── ${display} ─────────────────────────────────`);
1150
- for (const ln of localKeys) {
1151
- if (ln.type !== "kv") continue;
1152
- if (opts.annotate) out.push(`# from ${display}`);
1153
- out.push(`${ln.key}=`);
1154
- out.push("");
1493
+ const hint = ln.trailing.replace(/^\s*#\s*/, "").trim();
1494
+ out.push(hint.length > 0 ? {
1495
+ name: ln.key,
1496
+ hint
1497
+ } : { name: ln.key });
1155
1498
  }
1156
1499
  }
1157
- return out.join("\n");
1500
+ return out;
1158
1501
  }
1159
1502
  function countKeys(rendered) {
1160
1503
  return rendered.split("\n").filter((l) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(l)).length;
@@ -1499,4 +1842,4 @@ function createCli(argvInput) {
1499
1842
  //#endregion
1500
1843
  export { createCli as t };
1501
1844
 
1502
- //# sourceMappingURL=commands-Cg8YMJHj.js.map
1845
+ //# sourceMappingURL=commands-B70xh15E.js.map