@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.
- package/bin/cmd-enclave.ts +38 -30
- package/dist/cli.mjs +63 -35
- package/package.json +1 -1
- 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));
|
|
@@ -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
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
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
|
-
|
|
4257
|
-
|
|
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(
|
|
4352
|
-
console.log("
|
|
4353
|
-
console.log("
|
|
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
|
|
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
|
-
|
|
4417
|
-
|
|
4418
|
-
if (env.t3BaseUrl)
|
|
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
|
-
|
|
4453
|
+
lines.push(`tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
|
|
4425
4454
|
} catch (e) {
|
|
4426
|
-
|
|
4427
|
-
|
|
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
|
-
|
|
4435
|
-
Sealed secrets (${latest.size}):`);
|
|
4463
|
+
lines.push("", c.bold(`Sealed secrets (${latest.size})`));
|
|
4436
4464
|
if (latest.size === 0) {
|
|
4437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4444
|
-
|
|
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(
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
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
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();
|