feat(cli+broker): structured argument validation, msg-status prefixes (v1.9.3)
Adds apps/cli/src/cli/validators.ts — a small module of shape validators (pubkey, pubkey prefix, message id, mesh slug) that return discriminated results so callers can distinguish "shape is wrong" (INVALID_ARGS exit) from "value is well-shaped, lookup failed" (NOT_FOUND exit). Includes renderValidationError() for a consistent three-tier error contract: what's wrong, what would be valid, closest valid alternative. First adopter is `claudemesh msg-status`: - Validates id locally before opening WS — typos return immediately. - Accepts 8-32 char prefixes (full ids are 32). Pastes that get copy-truncated by the terminal still work. - Distinct error messages for malformed input vs not-in-queue vs ambiguous prefix; --json emits the structured shape. Broker side: WS message_status handler validates idStr is 8-32 base62 before querying. Prefix lookups use LIKE 'prefix%' scoped to the caller's mesh (no cross-mesh leak). Returns ambiguous_prefix when more than one match. Establishes the canonical pattern; rolling out to send / grant / revoke / topic post --reply-to in subsequent patches.
This commit is contained in:
@@ -20,6 +20,7 @@ import { tryBridge } from "~/services/bridge/client.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
import { bold, clay, dim } from "~/ui/styles.js";
|
||||
import { EXIT } from "~/constants/exit-codes.js";
|
||||
import { validateMessageId, renderValidationError } from "~/cli/validators.js";
|
||||
|
||||
type StateFlags = { mesh?: string; json?: boolean };
|
||||
type PeerStatus = "idle" | "working" | "dnd";
|
||||
@@ -186,15 +187,50 @@ export async function runForget(id: string | undefined, opts: StateFlags): Promi
|
||||
// --- msg-status ---
|
||||
|
||||
export async function runMsgStatus(id: string | undefined, opts: StateFlags): Promise<number> {
|
||||
if (!id) {
|
||||
render.err("Usage: claudemesh msg-status <message-id>");
|
||||
// Validate input shape *before* we open a WS connection, so a typo
|
||||
// returns a structured error instead of "not found or timed out".
|
||||
const v = validateMessageId(id);
|
||||
if (!v.ok) {
|
||||
if (opts.json) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: "invalid_argument",
|
||||
field: "messageId",
|
||||
code: v.code,
|
||||
reason: v.reason,
|
||||
expected: v.expected,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
renderValidationError({
|
||||
verb: "msg-status",
|
||||
input: id ?? "(missing)",
|
||||
result: v,
|
||||
});
|
||||
}
|
||||
return EXIT.INVALID_ARGS;
|
||||
}
|
||||
const lookupId = v.value.value;
|
||||
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
const result = await client.messageStatus(id);
|
||||
const result = await client.messageStatus(lookupId);
|
||||
if (!result) {
|
||||
if (opts.json) console.log(JSON.stringify({ id, found: false }));
|
||||
else render.err(`Message ${id} not found or timed out.`);
|
||||
if (opts.json) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: "not_found",
|
||||
id: lookupId,
|
||||
isPrefix: v.value.isPrefix,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const hint = v.value.isPrefix
|
||||
? ` no message id starts with ${dim("\"" + lookupId + "\"")} in this mesh.\n try: claudemesh msg-status <full-32-char-id>`
|
||||
: ` message ${dim(lookupId.slice(0, 12) + "…")} not in queue (already drained, expired, or never sent in this mesh).`;
|
||||
render.err(`message not found`);
|
||||
process.stderr.write(hint + "\n");
|
||||
}
|
||||
return EXIT.NOT_FOUND;
|
||||
}
|
||||
if (opts.json) {
|
||||
|
||||
Reference in New Issue
Block a user