Skip to content

Commit c601761

Browse files
committed
fix(cli): keep CLI display/approval truncation UTF-16 safe
Replace raw .slice(0, N) with truncateUtf16Safe in CLI display paths that may truncate user-provided text containing emoji or CJK characters: - cli/error-format: JSON input preview @45 - cli/exec-approvals-cli: approval display text @300 - commands/onboarding-plugin-install: plugin description @179 - commands/onboard-helpers: TUI display line @119
1 parent 7c0a7c8 commit c601761

4 files changed

Lines changed: 71 additions & 17 deletions

File tree

src/cli/error-format.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Reusable CLI error-message formatters that keep recovery hints consistent across commands.
2+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
23
import { formatCliCommand } from "./command-format.js";
34

45
const DEFAULT_GATEWAY_PORT_EXAMPLE = 18789;
@@ -59,7 +60,7 @@ export function formatStrictJsonParseFailure(params: { value: string; cause: unk
5960
const rawCause = params.cause instanceof Error ? params.cause.message : String(params.cause);
6061
const cause = rawCause.trim().replace(/[.]+$/u, "");
6162
const preview =
62-
params.value.length > 48 ? `${params.value.slice(0, 45).trimEnd()}...` : params.value;
63+
params.value.length > 48 ? `${truncateUtf16Safe(params.value, 45).trimEnd()}...` : params.value;
6364
return [
6465
`Could not parse ${JSON.stringify(preview)} as JSON for --strict-json.`,
6566
`${cause}.`,

src/cli/exec-approvals-cli.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// CLI for reading and mutating exec approval allowlists locally, via gateway, or via node.
22
import fs from "node:fs/promises";
33
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
4+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
45
import type { Command } from "commander";
56
import JSON5 from "json5";
67
import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js";
@@ -180,7 +181,7 @@ function formatCliError(err: unknown): string {
180181
const msg = formatErrorMessage(err);
181182
const firstLine = msg.includes("\n") ? msg.split("\n")[0] : msg;
182183
const safe = sanitizeForLog(firstLine);
183-
return safe.length > 300 ? `${safe.slice(0, 300)}...` : safe;
184+
return safe.length > 300 ? `${truncateUtf16Safe(safe, 300)}...` : safe;
184185
}
185186

186187
async function loadConfigForApprovalsTarget(params: {
@@ -306,8 +307,12 @@ function renderApprovalsSnapshot(snapshot: ExecApprovalsSnapshot, targetLabel: s
306307
: null,
307308
].filter((part): part is string => part != null);
308309
const agents = file.agents ?? {};
309-
const allowlistRows: Array<{ Target: string; Agent: string; Pattern: string; LastUsed: string }> =
310-
[];
310+
const allowlistRows: Array<{
311+
Target: string;
312+
Agent: string;
313+
Pattern: string;
314+
LastUsed: string;
315+
}> = [];
311316
const now = Date.now();
312317
for (const [agentId, agent] of Object.entries(agents)) {
313318
const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : [];
@@ -333,7 +338,10 @@ function renderApprovalsSnapshot(snapshot: ExecApprovalsSnapshot, targetLabel: s
333338
{ Field: "Hash", Value: snapshot.hash },
334339
{ Field: "Version", Value: String(file.version ?? 1) },
335340
{ Field: "Socket", Value: file.socket?.path ?? "default" },
336-
{ Field: "Defaults", Value: defaultsParts.length > 0 ? defaultsParts.join(", ") : "none" },
341+
{
342+
Field: "Defaults",
343+
Value: defaultsParts.length > 0 ? defaultsParts.join(", ") : "none",
344+
},
337345
{ Field: "Agents", Value: String(Object.keys(agents).length) },
338346
{ Field: "Allowlist", Value: String(allowlistRows.length) },
339347
];
@@ -431,7 +439,16 @@ async function loadWritableAllowlistAgent(opts: ExecApprovalsCliOpts): Promise<{
431439
const agent = ensureAgent(file, agentKey);
432440
const allowlistEntries = Array.isArray(agent.allowlist) ? agent.allowlist : [];
433441

434-
return { nodeId, source, targetLabel, baseHash, file, agentKey, agent, allowlistEntries };
442+
return {
443+
nodeId,
444+
source,
445+
targetLabel,
446+
baseHash,
447+
file,
448+
agentKey,
449+
agent,
450+
allowlistEntries,
451+
};
435452
}
436453

437454
type WritableAllowlistAgentContext = Awaited<ReturnType<typeof loadWritableAllowlistAgent>> & {
@@ -557,7 +574,14 @@ export function registerExecApprovalsCli(program: Command) {
557574
exitWithError(`Failed to parse approvals JSON: ${String(err)}`);
558575
}
559576
file.version = 1;
560-
await saveSnapshotTargeted({ opts, source, nodeId, file, baseHash, targetLabel });
577+
await saveSnapshotTargeted({
578+
opts,
579+
source,
580+
nodeId,
581+
file,
582+
baseHash,
583+
targetLabel,
584+
});
561585
} catch (err) {
562586
defaultRuntime.error(formatCliError(err));
563587
defaultRuntime.exit(1);
@@ -595,7 +619,10 @@ export function registerExecApprovalsCli(program: Command) {
595619
defaultRuntime.log("Already allowlisted.");
596620
return false;
597621
}
598-
allowlistEntries.push({ pattern: trimmedPattern, lastUsedAt: Date.now() });
622+
allowlistEntries.push({
623+
pattern: trimmedPattern,
624+
lastUsedAt: Date.now(),
625+
});
599626
agent.allowlist = allowlistEntries;
600627
file.agents = { ...file.agents, [agentKey]: agent };
601628
return true;

src/commands/onboard-helpers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cancel, isCancel } from "@clack/prompts";
66
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
77
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
88
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
9+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
910
import { visibleWidth } from "../../packages/terminal-core/src/ansi.js";
1011
import {
1112
decorativeEmoji,
@@ -325,7 +326,11 @@ export async function handleReset(scope: ResetScope, workspaceDir: string, runti
325326
for (const [index, attestationPath] of resolveWorkspaceAttestationPaths(
326327
workspaceDir,
327328
).entries()) {
328-
if (await shouldRemoveWorkspaceAttestation(attestationPath, { trustUnknown: index === 0 })) {
329+
if (
330+
await shouldRemoveWorkspaceAttestation(attestationPath, {
331+
trustUnknown: index === 0,
332+
})
333+
) {
329334
await moveToTrash(attestationPath, runtime);
330335
}
331336
}
@@ -410,7 +415,7 @@ function summarizeError(err: unknown): string {
410415
.split("\n")
411416
.map((s) => s.trim())
412417
.find(Boolean) ?? raw;
413-
return line.length > 120 ? `${line.slice(0, 119)}…` : line;
418+
return line.length > 120 ? `${truncateUtf16Safe(line, 119)}…` : line;
414419
}
415420

416421
/** Default workspace path shown by onboarding prompts. */

src/commands/onboarding-plugin-install.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import fs from "node:fs";
88
import path from "node:path";
99
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
10+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
1011
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
1112
import { resolveBundledInstallPlanForCatalogEntry } from "../cli/plugin-install-plan.js";
1213
import { invalidatePluginRuntimeDiscoveryAfterConfigMutation } from "../cli/plugins-registry-refresh.js";
@@ -302,7 +303,9 @@ function resolveBundledLocalPath(params: {
302303
entry: OnboardingPluginInstallEntry;
303304
workspaceDir?: string;
304305
}): string | null {
305-
const bundledSources = resolveBundledPluginSources({ workspaceDir: params.workspaceDir });
306+
const bundledSources = resolveBundledPluginSources({
307+
workspaceDir: params.workspaceDir,
308+
});
306309
const npmSpec = params.entry.install.npmSpec?.trim();
307310
if (npmSpec) {
308311
return (
@@ -498,10 +501,14 @@ async function promptInstallChoice(params: {
498501
function formatDurationLabel(timeoutMs: number): string {
499502
if (timeoutMs % 60_000 === 0) {
500503
const minutes = timeoutMs / 60_000;
501-
return t(minutes === 1 ? "common.minute" : "common.minutes", { count: minutes });
504+
return t(minutes === 1 ? "common.minute" : "common.minutes", {
505+
count: minutes,
506+
});
502507
}
503508
const seconds = Math.round(timeoutMs / 1000);
504-
return t(seconds === 1 ? "common.second" : "common.seconds", { count: seconds });
509+
return t(seconds === 1 ? "common.second" : "common.seconds", {
510+
count: seconds,
511+
});
505512
}
506513

507514
function formatPluginInstallProgress(label: string): string {
@@ -537,7 +544,7 @@ function summarizeInstallError(message: string): string {
537544
if (!cleaned) {
538545
return "Unknown install failure";
539546
}
540-
return cleaned.length > 180 ? `${cleaned.slice(0, 179)}…` : cleaned;
547+
return cleaned.length > 180 ? `${truncateUtf16Safe(cleaned, 179)}…` : cleaned;
541548
}
542549

543550
function isTimeoutError(error: unknown): boolean {
@@ -1099,7 +1106,9 @@ export async function ensureOnboardingPluginInstalled(params: {
10991106
}): Promise<OnboardingPluginInstallResult> {
11001107
const { entry, prompter, runtime, workspaceDir } = params;
11011108
let next = params.cfg;
1102-
const installOverride = resolvePluginInstallOverride({ pluginId: entry.pluginId });
1109+
const installOverride = resolvePluginInstallOverride({
1110+
pluginId: entry.pluginId,
1111+
});
11031112
if (installOverride) {
11041113
// Any install override mutates config/install records, so guard it with the
11051114
// same write-mode check as normal installs.
@@ -1211,7 +1220,13 @@ export async function ensureOnboardingPluginInstalled(params: {
12111220
);
12121221
}
12131222
next = addPluginLoadPath(enableResult.config, localPath);
1214-
next = await recordLocalPluginInstall({ cfg: next, entry, localPath, npmSpec, workspaceDir });
1223+
next = await recordLocalPluginInstall({
1224+
cfg: next,
1225+
entry,
1226+
localPath,
1227+
npmSpec,
1228+
workspaceDir,
1229+
});
12151230
return await markOnboardingPluginInstalled(
12161231
{
12171232
cfg: next,
@@ -1457,7 +1472,13 @@ export async function ensureOnboardingPluginInstalled(params: {
14571472
);
14581473
}
14591474
next = addPluginLoadPath(enableResult.config, localPath);
1460-
next = await recordLocalPluginInstall({ cfg: next, entry, localPath, npmSpec, workspaceDir });
1475+
next = await recordLocalPluginInstall({
1476+
cfg: next,
1477+
entry,
1478+
localPath,
1479+
npmSpec,
1480+
workspaceDir,
1481+
});
14611482
return await markOnboardingPluginInstalled(
14621483
{
14631484
cfg: next,

0 commit comments

Comments
 (0)