@fiscalmindset/blindfold 0.4.3 โ†’ 0.4.4

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.
@@ -16,6 +16,7 @@ import { runCompat } from "../src/compat.ts";
16
16
  import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
17
  import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
18
  import { c, ok, bad, warn, head } from "../src/color.ts";
19
+ import { boxLines, rule } from "../src/tui.ts";
19
20
 
20
21
  export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
21
22
  switch (cmd) {
@@ -32,15 +33,19 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
32
33
  const b = await client.getBalance();
33
34
  const tok = (n: number) => (n / BASE).toLocaleString(undefined, { maximumFractionDigits: 6 });
34
35
  if (argv.flags.json) { console.log(JSON.stringify(b, null, 2)); return; }
35
- console.log(head(`๐Ÿ’ณ Terminal 3 credit`) + c.gray(` โ€” ${env.did || "(no tenant)"} (${env.mock ? "MOCK" : env.t3Env})`));
36
- console.log(` available: ${c.bold(tok(b.available) + " tokens")} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`);
37
- console.log(` reserved: ${b.reserved.toLocaleString()} base units`);
38
- console.log(` status: ${b.creditExhausted ? warn("โš  EXHAUSTED") : ok("โœ… ok")}`);
36
+ const lines = [
37
+ c.gray(`${env.did || "(no tenant)"} ยท ${env.mock ? "MOCK" : env.t3Env}`),
38
+ "",
39
+ `available: ${c.bold(c.green(tok(b.available) + " tokens"))} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`,
40
+ `reserved: ${b.reserved.toLocaleString()} base units`,
41
+ `status: ${b.creditExhausted ? warn("โš  EXHAUSTED") : ok("โœ… ok")}`,
42
+ ];
39
43
  if (b.creditExhausted) {
40
- console.log(` Top up testnet credits, then re-check with \`blindfold credit\`:`);
41
- console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
44
+ lines.push("", c.yellow("Top up testnet credits, then re-check with `blindfold credit`:"),
45
+ c.cyan(" https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens"));
42
46
  process.exitCode = 1;
43
47
  }
48
+ console.log(boxLines("๐Ÿ’ณ Terminal 3 credit", lines));
44
49
  } finally {
45
50
  await client.close();
46
51
  }
@@ -136,9 +141,10 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
136
141
  console.log(`Seal one with: blindfold register --name <KV_KEY>`);
137
142
  return;
138
143
  }
139
- console.log(head("๐Ÿ” Sealed keys") + c.gray(` (source: ${defaultSealedLogPath()})`) + "\n");
140
- console.log(" WHEN NAME BYTES MODE WHERE");
141
- console.log(" โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€");
144
+ console.log(boxLines("๐Ÿ” Sealed keys", [c.gray(`source: ${defaultSealedLogPath()}`)]));
145
+ console.log("");
146
+ console.log(c.gray(" WHEN NAME BYTES MODE WHERE"));
147
+ console.log(c.gray(" โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€"));
142
148
  for (const e of entries) {
143
149
  const when = e.t.replace("T", " ").slice(0, 19);
144
150
  const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "โ€ฆ" : e.map_name;
@@ -150,9 +156,10 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
150
156
  case "audit": {
151
157
  // (1) Tamper-evidence: verify the local ledger's hash-chain.
152
158
  // (2) Reconcile against the enclave โ€” the actual source of truth.
153
- console.log("๐Ÿ” Blindfold audit\n");
159
+ console.log(boxLines("๐Ÿ” Blindfold audit", [c.gray("ledger hash-chain + reconciliation against the enclave")]));
160
+ console.log("");
154
161
  const chain = verifyLedgerChain();
155
- console.log(" 1. Ledger integrity (tamper-evidence)");
162
+ console.log(c.bold(" 1. Ledger integrity (tamper-evidence)"));
156
163
  if (chain.total === 0) {
157
164
  console.log(" (ledger is empty)");
158
165
  } else if (chain.ok) {
@@ -203,44 +210,46 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
203
210
  case "status": {
204
211
  // One-glance overview: health + sealed inventory + what to do next.
205
212
  const env = loadBlindfoldEnv();
206
- console.log(head("๐Ÿ›ก๏ธ Blindfold status") + "\n");
207
- console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} ยท T3 env: ${env.t3Env}`);
208
- if (env.t3BaseUrl) console.log(` node: ${env.t3BaseUrl} (override)`);
213
+ const lines: string[] = [];
214
+ lines.push(`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} ยท T3 env: ${env.t3Env}`);
215
+ if (env.t3BaseUrl) lines.push(`node: ${env.t3BaseUrl} (override)`);
209
216
  if (!env.mock) {
210
217
  try {
211
218
  const { openT3Client } = await import("../src/t3-client.ts");
212
219
  const client = await openT3Client(env);
213
220
  const info = await client.me();
214
- console.log(` tenant: ${ok("โœ… " + info.tenant)} (status=${info.status ?? "?"})`);
221
+ lines.push(`tenant: ${ok("โœ… " + info.tenant)} (status=${info.status ?? "?"})`);
215
222
  } catch (e) {
216
- console.log(` tenant: ${bad("โœ– " + (e as Error).message.slice(0, 90))}`);
217
- console.log(` โ†’ run \`blindfold doctor\` for a full diagnosis.`);
223
+ lines.push(`tenant: ${bad("โœ– " + (e as Error).message.slice(0, 90))}`);
224
+ lines.push(` ${c.gray("โ†’ run `blindfold doctor` for a full diagnosis.")}`);
218
225
  process.exitCode = 1;
219
226
  }
220
227
  }
221
228
  const entries = readSealed();
222
229
  const latest = new Map<string, (typeof entries)[number]>();
223
230
  for (const e of entries) latest.set(e.name, e); // last write wins
224
- console.log(`\n Sealed secrets (${latest.size}):`);
231
+ lines.push("", c.bold(`Sealed secrets (${latest.size})`));
225
232
  if (latest.size === 0) {
226
- console.log(` (none yet) seal one: blindfold register --name <X> --from-env <X>`);
233
+ lines.push(` ${c.gray("(none yet) seal one:")} ${c.cyan("blindfold register --name <X>")}`);
227
234
  } else {
228
235
  for (const e of latest.values()) {
229
- console.log(` โ€ข ${e.name.padEnd(22)} ${String(e.length).padStart(4)} B ${e.mode}`);
236
+ lines.push(` ${c.green("โ€ข")} ${c.cyan(e.name.padEnd(22))} ${String(e.length).padStart(4)} B ${c.gray(e.mode)}`);
230
237
  }
231
238
  }
232
- console.log(`\n Next: blindfold use --name <secret> -- <command> (use it, no code)`);
239
+ lines.push("", c.gray("Next: ") + c.cyan("blindfold use --name <secret> -- <command>") + c.gray(" (use it, no code)"));
240
+ console.log(boxLines("๐Ÿ›ก๏ธ Blindfold status", lines));
233
241
  return;
234
242
  }
235
243
  case "doctor": {
236
244
  const env = loadBlindfoldEnv();
237
- console.log(head("๐Ÿฉบ Blindfold doctor"));
238
- console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`);
239
- console.log(` T3N_API_KEY set: ${env.t3nApiKey ? "yes" : "NO โœ–"}`);
240
- console.log(` DID set: ${env.did ? "yes" : "NO โœ–"}`);
241
- console.log(` T3 environment: ${env.t3Env}`);
242
- console.log(` node URL: ${env.t3BaseUrl || `(SDK default for ${env.t3Env})`}`);
243
- console.log(` default proxy port: ${env.port}`);
245
+ console.log(boxLines("๐Ÿฉบ Blindfold doctor", [
246
+ `mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`,
247
+ `T3N_API_KEY: ${env.t3nApiKey ? ok("set") : bad("NOT set โœ–")}`,
248
+ `DID: ${env.did ? ok("set") : bad("NOT set โœ–")}`,
249
+ `T3 env: ${env.t3Env}`,
250
+ `node URL: ${env.t3BaseUrl || c.gray(`(SDK default for ${env.t3Env})`)}`,
251
+ `proxy port: ${env.port}`,
252
+ ]));
244
253
  if (!env.mock && (!env.t3nApiKey || !env.did)) {
245
254
  console.log("");
246
255
  console.log(` โš  REAL mode is selected but credentials are missing.`);
@@ -254,8 +263,7 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
254
263
  // LIVE check: authenticate, then read the tenant behind the key. This is
255
264
  // what catches the painful "key authenticates but has no tenant" case,
256
265
  // which the server otherwise reports only as a bare HTTP 500.
257
- console.log("");
258
- console.log(" Live check (handshake + authenticate + me) โ€ฆ");
266
+ console.log("\n" + rule("Live check โ€” handshake + authenticate + me"));
259
267
  const { openT3Client } = await import("../src/t3-client.ts");
260
268
  let client;
261
269
  try {
package/dist/cli.mjs CHANGED
@@ -1309,6 +1309,7 @@ var c = {
1309
1309
  cyan: wrap("36"),
1310
1310
  gray: wrap("90")
1311
1311
  };
1312
+ var colorOn = on;
1312
1313
  var ok = (s) => c.green(s);
1313
1314
  var bad = (s) => c.red(s);
1314
1315
  var warn = (s) => c.yellow(s);
@@ -1434,12 +1435,31 @@ function boxLines(title, lines) {
1434
1435
  const w = termWidth();
1435
1436
  const inner = w - 4;
1436
1437
  const prefix = B.tl + B.h + " ";
1437
- const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
1438
- const out = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
1439
- for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
1438
+ const fill = Math.max(0, w - vlen(prefix) - vlen(clip(title, inner)) - 2);
1439
+ const out = [dim(prefix) + c.bold(clip(title, inner)) + dim(" " + B.h.repeat(fill) + B.tr)];
1440
+ for (const l of lines) out.push(dim(B.v) + " " + pad(clip(l, inner), inner) + " " + dim(B.v));
1440
1441
  out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
1441
1442
  return out.join("\n");
1442
1443
  }
1444
+ function clip(s, width) {
1445
+ if (vlen(s) <= width) return s;
1446
+ let out = "";
1447
+ let wSoFar = 0;
1448
+ for (const part of s.split(/(\x1b\[[0-9;]*m)/)) {
1449
+ if (part.startsWith("\x1B")) {
1450
+ out += part;
1451
+ continue;
1452
+ }
1453
+ for (const ch of part) {
1454
+ const cp = ch.codePointAt(0) ?? 0;
1455
+ const cw = cp === 8205 || cp >= 65024 && cp <= 65039 || cp >= 768 && cp <= 879 ? 0 : isWide(cp) ? 2 : 1;
1456
+ if (wSoFar + cw > width - 1) return out + "\u2026" + (colorOn ? "\x1B[0m" : "");
1457
+ out += ch;
1458
+ wSoFar += cw;
1459
+ }
1460
+ }
1461
+ return out + (colorOn ? "\x1B[0m" : "");
1462
+ }
1443
1463
  function rule(label = "") {
1444
1464
  const w = termWidth();
1445
1465
  if (!label) return dim(B.h.repeat(w));
@@ -4248,15 +4268,22 @@ async function handleEnclave(cmd, argv, cmdArgs) {
4248
4268
  console.log(JSON.stringify(b, null, 2));
4249
4269
  return;
4250
4270
  }
4251
- console.log(head(`\u{1F4B3} Terminal 3 credit`) + c.gray(` \u2014 ${env.did || "(no tenant)"} (${env.mock ? "MOCK" : env.t3Env})`));
4252
- console.log(` available: ${c.bold(tok(b.available) + " tokens")} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`);
4253
- console.log(` reserved: ${b.reserved.toLocaleString()} base units`);
4254
- console.log(` status: ${b.creditExhausted ? warn("\u26A0 EXHAUSTED") : ok("\u2705 ok")}`);
4271
+ const lines = [
4272
+ c.gray(`${env.did || "(no tenant)"} \xB7 ${env.mock ? "MOCK" : env.t3Env}`),
4273
+ "",
4274
+ `available: ${c.bold(c.green(tok(b.available) + " tokens"))} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`,
4275
+ `reserved: ${b.reserved.toLocaleString()} base units`,
4276
+ `status: ${b.creditExhausted ? warn("\u26A0 EXHAUSTED") : ok("\u2705 ok")}`
4277
+ ];
4255
4278
  if (b.creditExhausted) {
4256
- console.log(` Top up testnet credits, then re-check with \`blindfold credit\`:`);
4257
- console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
4279
+ lines.push(
4280
+ "",
4281
+ c.yellow("Top up testnet credits, then re-check with `blindfold credit`:"),
4282
+ c.cyan(" https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens")
4283
+ );
4258
4284
  process.exitCode = 1;
4259
4285
  }
4286
+ console.log(boxLines("\u{1F4B3} Terminal 3 credit", lines));
4260
4287
  } finally {
4261
4288
  await client.close();
4262
4289
  }
@@ -4348,9 +4375,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
4348
4375
  console.log(`Seal one with: blindfold register --name <KV_KEY>`);
4349
4376
  return;
4350
4377
  }
4351
- console.log(head("\u{1F510} Sealed keys") + c.gray(` (source: ${defaultSealedLogPath()})`) + "\n");
4352
- console.log(" WHEN NAME BYTES MODE WHERE");
4353
- console.log(" \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500");
4378
+ console.log(boxLines("\u{1F510} Sealed keys", [c.gray(`source: ${defaultSealedLogPath()}`)]));
4379
+ console.log("");
4380
+ console.log(c.gray(" WHEN NAME BYTES MODE WHERE"));
4381
+ console.log(c.gray(" \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500"));
4354
4382
  for (const e of entries) {
4355
4383
  const when = e.t.replace("T", " ").slice(0, 19);
4356
4384
  const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "\u2026" : e.map_name;
@@ -4360,9 +4388,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
4360
4388
  return;
4361
4389
  }
4362
4390
  case "audit": {
4363
- console.log("\u{1F50D} Blindfold audit\n");
4391
+ console.log(boxLines("\u{1F50D} Blindfold audit", [c.gray("ledger hash-chain + reconciliation against the enclave")]));
4392
+ console.log("");
4364
4393
  const chain = verifyLedgerChain();
4365
- console.log(" 1. Ledger integrity (tamper-evidence)");
4394
+ console.log(c.bold(" 1. Ledger integrity (tamper-evidence)"));
4366
4395
  if (chain.total === 0) {
4367
4396
  console.log(" (ledger is empty)");
4368
4397
  } else if (chain.ok) {
@@ -4413,46 +4442,46 @@ async function handleEnclave(cmd, argv, cmdArgs) {
4413
4442
  }
4414
4443
  case "status": {
4415
4444
  const env = loadBlindfoldEnv();
4416
- console.log(head("\u{1F6E1}\uFE0F Blindfold status") + "\n");
4417
- console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} \xB7 T3 env: ${env.t3Env}`);
4418
- if (env.t3BaseUrl) console.log(` node: ${env.t3BaseUrl} (override)`);
4445
+ const lines = [];
4446
+ lines.push(`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} \xB7 T3 env: ${env.t3Env}`);
4447
+ if (env.t3BaseUrl) lines.push(`node: ${env.t3BaseUrl} (override)`);
4419
4448
  if (!env.mock) {
4420
4449
  try {
4421
4450
  const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
4422
4451
  const client = await openT3Client2(env);
4423
4452
  const info2 = await client.me();
4424
- console.log(` tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
4453
+ lines.push(`tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
4425
4454
  } catch (e) {
4426
- console.log(` tenant: ${bad("\u2716 " + e.message.slice(0, 90))}`);
4427
- console.log(` \u2192 run \`blindfold doctor\` for a full diagnosis.`);
4455
+ lines.push(`tenant: ${bad("\u2716 " + e.message.slice(0, 90))}`);
4456
+ lines.push(` ${c.gray("\u2192 run `blindfold doctor` for a full diagnosis.")}`);
4428
4457
  process.exitCode = 1;
4429
4458
  }
4430
4459
  }
4431
4460
  const entries = readSealed();
4432
4461
  const latest = /* @__PURE__ */ new Map();
4433
4462
  for (const e of entries) latest.set(e.name, e);
4434
- console.log(`
4435
- Sealed secrets (${latest.size}):`);
4463
+ lines.push("", c.bold(`Sealed secrets (${latest.size})`));
4436
4464
  if (latest.size === 0) {
4437
- console.log(` (none yet) seal one: blindfold register --name <X> --from-env <X>`);
4465
+ lines.push(` ${c.gray("(none yet) seal one:")} ${c.cyan("blindfold register --name <X>")}`);
4438
4466
  } else {
4439
4467
  for (const e of latest.values()) {
4440
- console.log(` \u2022 ${e.name.padEnd(22)} ${String(e.length).padStart(4)} B ${e.mode}`);
4468
+ lines.push(` ${c.green("\u2022")} ${c.cyan(e.name.padEnd(22))} ${String(e.length).padStart(4)} B ${c.gray(e.mode)}`);
4441
4469
  }
4442
4470
  }
4443
- console.log(`
4444
- Next: blindfold use --name <secret> -- <command> (use it, no code)`);
4471
+ lines.push("", c.gray("Next: ") + c.cyan("blindfold use --name <secret> -- <command>") + c.gray(" (use it, no code)"));
4472
+ console.log(boxLines("\u{1F6E1}\uFE0F Blindfold status", lines));
4445
4473
  return;
4446
4474
  }
4447
4475
  case "doctor": {
4448
4476
  const env = loadBlindfoldEnv();
4449
- console.log(head("\u{1FA7A} Blindfold doctor"));
4450
- console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`);
4451
- console.log(` T3N_API_KEY set: ${env.t3nApiKey ? "yes" : "NO \u2716"}`);
4452
- console.log(` DID set: ${env.did ? "yes" : "NO \u2716"}`);
4453
- console.log(` T3 environment: ${env.t3Env}`);
4454
- console.log(` node URL: ${env.t3BaseUrl || `(SDK default for ${env.t3Env})`}`);
4455
- console.log(` default proxy port: ${env.port}`);
4477
+ console.log(boxLines("\u{1FA7A} Blindfold doctor", [
4478
+ `mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`,
4479
+ `T3N_API_KEY: ${env.t3nApiKey ? ok("set") : bad("NOT set \u2716")}`,
4480
+ `DID: ${env.did ? ok("set") : bad("NOT set \u2716")}`,
4481
+ `T3 env: ${env.t3Env}`,
4482
+ `node URL: ${env.t3BaseUrl || c.gray(`(SDK default for ${env.t3Env})`)}`,
4483
+ `proxy port: ${env.port}`
4484
+ ]));
4456
4485
  if (!env.mock && (!env.t3nApiKey || !env.did)) {
4457
4486
  console.log("");
4458
4487
  console.log(` \u26A0 REAL mode is selected but credentials are missing.`);
@@ -4462,8 +4491,7 @@ async function handleEnclave(cmd, argv, cmdArgs) {
4462
4491
  return;
4463
4492
  }
4464
4493
  if (env.mock) return;
4465
- console.log("");
4466
- console.log(" Live check (handshake + authenticate + me) \u2026");
4494
+ console.log("\n" + rule("Live check \u2014 handshake + authenticate + me"));
4467
4495
  const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
4468
4496
  let client;
4469
4497
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscalmindset/blindfold",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "description": "Make any AI agent's API keys un-leakable, by sealing them inside a Terminal 3 TDX enclave.",
6
6
  "license": "MIT",
package/src/tui.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * wrapping, rounded boxes, and a two-column command table. Used to render a
4
4
  * structured `blindfold help` that reflows to the terminal.
5
5
  */
6
- import { c } from "./color.ts";
6
+ import { c, colorOn } from "./color.ts";
7
7
 
8
8
  /** Terminal width, clamped to a readable range. */
9
9
  export function termWidth(): number {
@@ -124,13 +124,32 @@ export function boxLines(title: string, lines: string[]): string {
124
124
  const w = termWidth();
125
125
  const inner = w - 4;
126
126
  const prefix = B.tl + B.h + " ";
127
- const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
128
- const out: string[] = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
129
- for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
127
+ const fill = Math.max(0, w - vlen(prefix) - vlen(clip(title, inner)) - 2);
128
+ const out: string[] = [dim(prefix) + c.bold(clip(title, inner)) + dim(" " + B.h.repeat(fill) + B.tr)];
129
+ for (const l of lines) out.push(dim(B.v) + " " + pad(clip(l, inner), inner) + " " + dim(B.v));
130
130
  out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
131
131
  return out.join("\n");
132
132
  }
133
133
 
134
+ /** Truncate a string to display width `width` (ANSI/emoji aware), appending โ€ฆ
135
+ * and a reset so a clipped color can't bleed. No-op if it already fits. */
136
+ export function clip(s: string, width: number): string {
137
+ if (vlen(s) <= width) return s;
138
+ let out = "";
139
+ let wSoFar = 0;
140
+ for (const part of s.split(/(\x1b\[[0-9;]*m)/)) {
141
+ if (part.startsWith("\x1b")) { out += part; continue; }
142
+ for (const ch of part) {
143
+ const cp = ch.codePointAt(0) ?? 0;
144
+ const cw = cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x300 && cp <= 0x36f) ? 0 : isWide(cp) ? 2 : 1;
145
+ if (wSoFar + cw > width - 1) return out + "โ€ฆ" + (colorOn ? "\x1b[0m" : "");
146
+ out += ch;
147
+ wSoFar += cw;
148
+ }
149
+ }
150
+ return out + (colorOn ? "\x1b[0m" : "");
151
+ }
152
+
134
153
  /** A plain full-width rule with an optional bold label: "โ”€โ”€ Label โ”€โ”€โ”€โ”€โ”€". */
135
154
  export function rule(label = ""): string {
136
155
  const w = termWidth();