@fiscalmindset/blindfold 0.4.3 โ 0.4.5
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/bin/cmd-enclave.ts +38 -30
- package/dist/cli.mjs +69 -37
- package/package.json +1 -1
- package/src/help.ts +7 -2
- package/src/tui.ts +23 -4
package/bin/cmd-enclave.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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(
|
|
140
|
-
console.log("
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
if (env.t3BaseUrl)
|
|
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
|
-
|
|
221
|
+
lines.push(`tenant: ${ok("โ
" + info.tenant)} (status=${info.status ?? "?"})`);
|
|
215
222
|
} catch (e) {
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
231
|
+
lines.push("", c.bold(`Sealed secrets (${latest.size})`));
|
|
225
232
|
if (latest.size === 0) {
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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));
|
|
@@ -1683,8 +1703,12 @@ function renderMainHelp() {
|
|
|
1683
1703
|
const dl = wrapText(cmd.summary, descW);
|
|
1684
1704
|
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
1685
1705
|
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1706
|
+
const gutter = pad("", nameW) + " ";
|
|
1707
|
+
if (cmd.usage) {
|
|
1708
|
+
lines.push(gutter + c.gray(pad("usage", 5)) + " " + c.cyan(fit(`blindfold ${cmd.name} ${cmd.usage}`, descW - 6)));
|
|
1709
|
+
}
|
|
1710
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 6);
|
|
1711
|
+
lines.push(gutter + c.gray(pad("e.g.", 5)) + " " + c.green(ex));
|
|
1688
1712
|
}
|
|
1689
1713
|
out.push(boxLines(group, lines));
|
|
1690
1714
|
out.push("");
|
|
@@ -4248,15 +4272,22 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4248
4272
|
console.log(JSON.stringify(b, null, 2));
|
|
4249
4273
|
return;
|
|
4250
4274
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4275
|
+
const lines = [
|
|
4276
|
+
c.gray(`${env.did || "(no tenant)"} \xB7 ${env.mock ? "MOCK" : env.t3Env}`),
|
|
4277
|
+
"",
|
|
4278
|
+
`available: ${c.bold(c.green(tok(b.available) + " tokens"))} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`,
|
|
4279
|
+
`reserved: ${b.reserved.toLocaleString()} base units`,
|
|
4280
|
+
`status: ${b.creditExhausted ? warn("\u26A0 EXHAUSTED") : ok("\u2705 ok")}`
|
|
4281
|
+
];
|
|
4255
4282
|
if (b.creditExhausted) {
|
|
4256
|
-
|
|
4257
|
-
|
|
4283
|
+
lines.push(
|
|
4284
|
+
"",
|
|
4285
|
+
c.yellow("Top up testnet credits, then re-check with `blindfold credit`:"),
|
|
4286
|
+
c.cyan(" https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens")
|
|
4287
|
+
);
|
|
4258
4288
|
process.exitCode = 1;
|
|
4259
4289
|
}
|
|
4290
|
+
console.log(boxLines("\u{1F4B3} Terminal 3 credit", lines));
|
|
4260
4291
|
} finally {
|
|
4261
4292
|
await client.close();
|
|
4262
4293
|
}
|
|
@@ -4348,9 +4379,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4348
4379
|
console.log(`Seal one with: blindfold register --name <KV_KEY>`);
|
|
4349
4380
|
return;
|
|
4350
4381
|
}
|
|
4351
|
-
console.log(
|
|
4352
|
-
console.log("
|
|
4353
|
-
console.log("
|
|
4382
|
+
console.log(boxLines("\u{1F510} Sealed keys", [c.gray(`source: ${defaultSealedLogPath()}`)]));
|
|
4383
|
+
console.log("");
|
|
4384
|
+
console.log(c.gray(" WHEN NAME BYTES MODE WHERE"));
|
|
4385
|
+
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
4386
|
for (const e of entries) {
|
|
4355
4387
|
const when = e.t.replace("T", " ").slice(0, 19);
|
|
4356
4388
|
const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "\u2026" : e.map_name;
|
|
@@ -4360,9 +4392,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4360
4392
|
return;
|
|
4361
4393
|
}
|
|
4362
4394
|
case "audit": {
|
|
4363
|
-
console.log("\u{1F50D} Blindfold audit
|
|
4395
|
+
console.log(boxLines("\u{1F50D} Blindfold audit", [c.gray("ledger hash-chain + reconciliation against the enclave")]));
|
|
4396
|
+
console.log("");
|
|
4364
4397
|
const chain = verifyLedgerChain();
|
|
4365
|
-
console.log(" 1. Ledger integrity (tamper-evidence)");
|
|
4398
|
+
console.log(c.bold(" 1. Ledger integrity (tamper-evidence)"));
|
|
4366
4399
|
if (chain.total === 0) {
|
|
4367
4400
|
console.log(" (ledger is empty)");
|
|
4368
4401
|
} else if (chain.ok) {
|
|
@@ -4413,46 +4446,46 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4413
4446
|
}
|
|
4414
4447
|
case "status": {
|
|
4415
4448
|
const env = loadBlindfoldEnv();
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
if (env.t3BaseUrl)
|
|
4449
|
+
const lines = [];
|
|
4450
|
+
lines.push(`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} \xB7 T3 env: ${env.t3Env}`);
|
|
4451
|
+
if (env.t3BaseUrl) lines.push(`node: ${env.t3BaseUrl} (override)`);
|
|
4419
4452
|
if (!env.mock) {
|
|
4420
4453
|
try {
|
|
4421
4454
|
const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
|
|
4422
4455
|
const client = await openT3Client2(env);
|
|
4423
4456
|
const info2 = await client.me();
|
|
4424
|
-
|
|
4457
|
+
lines.push(`tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
|
|
4425
4458
|
} catch (e) {
|
|
4426
|
-
|
|
4427
|
-
|
|
4459
|
+
lines.push(`tenant: ${bad("\u2716 " + e.message.slice(0, 90))}`);
|
|
4460
|
+
lines.push(` ${c.gray("\u2192 run `blindfold doctor` for a full diagnosis.")}`);
|
|
4428
4461
|
process.exitCode = 1;
|
|
4429
4462
|
}
|
|
4430
4463
|
}
|
|
4431
4464
|
const entries = readSealed();
|
|
4432
4465
|
const latest = /* @__PURE__ */ new Map();
|
|
4433
4466
|
for (const e of entries) latest.set(e.name, e);
|
|
4434
|
-
|
|
4435
|
-
Sealed secrets (${latest.size}):`);
|
|
4467
|
+
lines.push("", c.bold(`Sealed secrets (${latest.size})`));
|
|
4436
4468
|
if (latest.size === 0) {
|
|
4437
|
-
|
|
4469
|
+
lines.push(` ${c.gray("(none yet) seal one:")} ${c.cyan("blindfold register --name <X>")}`);
|
|
4438
4470
|
} else {
|
|
4439
4471
|
for (const e of latest.values()) {
|
|
4440
|
-
|
|
4472
|
+
lines.push(` ${c.green("\u2022")} ${c.cyan(e.name.padEnd(22))} ${String(e.length).padStart(4)} B ${c.gray(e.mode)}`);
|
|
4441
4473
|
}
|
|
4442
4474
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4475
|
+
lines.push("", c.gray("Next: ") + c.cyan("blindfold use --name <secret> -- <command>") + c.gray(" (use it, no code)"));
|
|
4476
|
+
console.log(boxLines("\u{1F6E1}\uFE0F Blindfold status", lines));
|
|
4445
4477
|
return;
|
|
4446
4478
|
}
|
|
4447
4479
|
case "doctor": {
|
|
4448
4480
|
const env = loadBlindfoldEnv();
|
|
4449
|
-
console.log(
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4481
|
+
console.log(boxLines("\u{1FA7A} Blindfold doctor", [
|
|
4482
|
+
`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`,
|
|
4483
|
+
`T3N_API_KEY: ${env.t3nApiKey ? ok("set") : bad("NOT set \u2716")}`,
|
|
4484
|
+
`DID: ${env.did ? ok("set") : bad("NOT set \u2716")}`,
|
|
4485
|
+
`T3 env: ${env.t3Env}`,
|
|
4486
|
+
`node URL: ${env.t3BaseUrl || c.gray(`(SDK default for ${env.t3Env})`)}`,
|
|
4487
|
+
`proxy port: ${env.port}`
|
|
4488
|
+
]));
|
|
4456
4489
|
if (!env.mock && (!env.t3nApiKey || !env.did)) {
|
|
4457
4490
|
console.log("");
|
|
4458
4491
|
console.log(` \u26A0 REAL mode is selected but credentials are missing.`);
|
|
@@ -4462,8 +4495,7 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4462
4495
|
return;
|
|
4463
4496
|
}
|
|
4464
4497
|
if (env.mock) return;
|
|
4465
|
-
console.log("");
|
|
4466
|
-
console.log(" Live check (handshake + authenticate + me) \u2026");
|
|
4498
|
+
console.log("\n" + rule("Live check \u2014 handshake + authenticate + me"));
|
|
4467
4499
|
const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
|
|
4468
4500
|
let client;
|
|
4469
4501
|
try {
|
package/package.json
CHANGED
package/src/help.ts
CHANGED
|
@@ -215,9 +215,14 @@ export function renderMainHelp(): string {
|
|
|
215
215
|
// command name (bright) + summary
|
|
216
216
|
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
217
217
|
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
218
|
+
const gutter = pad("", nameW) + " ";
|
|
219
|
+
// the shape โ with <placeholders> โ so you learn the pattern (only if it takes args)
|
|
220
|
+
if (cmd.usage) {
|
|
221
|
+
lines.push(gutter + c.gray(pad("usage", 5)) + " " + c.cyan(fit(`blindfold ${cmd.name} ${cmd.usage}`, descW - 6)));
|
|
222
|
+
}
|
|
218
223
|
// one concrete example โ the fastest way to "get" the command
|
|
219
|
-
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW -
|
|
220
|
-
lines.push(
|
|
224
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 6);
|
|
225
|
+
lines.push(gutter + c.gray(pad("e.g.", 5)) + " " + c.green(ex));
|
|
221
226
|
}
|
|
222
227
|
out.push(boxLines(group, lines));
|
|
223
228
|
out.push("");
|
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();
|