const VERSION = "0.2.0-rest"; const CLI_NAME = "device-pod-cli"; const DEFAULT_TIMEOUT_MS = 30000; type FetchLike = typeof fetch; type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; export async function main(argv = process.argv.slice(2), options: { env?: EnvLike; fetchImpl?: FetchLike; stdinText?: string; now?: () => string } = {}) { const result = await runDevicePodCli(argv, options); console.log(JSON.stringify(result.payload, null, 2)); process.exitCode = result.exitCode; } export async function runDevicePodCli(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; stdinText?: string; now?: () => string } = {}) { const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? fetch; const now = options.now ?? (() => new Date().toISOString()); try { const parsed = parseOptions(argv); const target = parsed._[0] || "help"; const rest = parsed._.slice(1); const context = { parsed, rest, env, fetchImpl, stdinText: options.stdinText, now }; const payload = await dispatch(target, context); return { exitCode: payload.ok === false ? 1 : 0, payload: withMeta(payload, now) }; } catch (error) { return { exitCode: 1, payload: withMeta(failure("device-pod-cli", error), now) }; } } async function dispatch(target: string, context: any) { if (["help", "--help", "-h"].includes(target)) return help(); if (target === "setup") return setupCommand(context); if (target === "admin") return adminCommand(context); if (target === "login") return login(context); if (target === "doctor") return doctor(context); if (target === "health") return health(context); if (target === "profile") return profileCommand(context); if (target === "lease" || target === "leases") return leaseCommand(context); if (target === "job" || target === "jobs") return jobCommand(context); const selector = parseSelector(target); if (!selector) throw cliError("invalid_target_selector", `invalid target selector: ${target}`); return invokeSelector(selector, context); } function help() { return ok("help", { version: VERSION, contractVersion: "device-pod-rest-cli-v1", profileAuthority: "hwlab-cloud-api", localProfileAuthority: false, configuration: { apiBaseUrl: "--api-base-url or HWLAB_DEVICE_POD_API_URL/HWLAB_CLOUD_API_URL", auth: "--session-token, --cookie, --bearer-token, or matching HWLAB_* env" }, usage: [ "device-pod-cli login --api-base-url URL --username USER --password PASS", "device-pod-cli setup first-admin --api-base-url URL --username USER --password PASS --pod-id device-pod-71-freq --profile-json JSON", "device-pod-cli admin device-pod upsert --api-base-url URL --session-token TOKEN --pod-id device-pod-71-freq --profile-json JSON", "device-pod-cli admin grant --api-base-url URL --session-token TOKEN --pod-id device-pod-71-freq --user-id USER_ID", "device-pod-cli profile list --api-base-url URL --session-token TOKEN", "device-pod-cli profile show --pod-id device-pod-71-freq --api-base-url URL --session-token TOKEN", "device-pod-cli device-pod-71-freq:workspace:/ ls --api-base-url URL --session-token TOKEN", "device-pod-cli device-pod-71-freq:workspace:/ put User/new.c --reason TEXT --lease-token TOKEN < file", "device-pod-cli device-pod-71-freq:workspace:/ rmdir User/empty --reason TEXT --lease-token TOKEN", "device-pod-cli device-pod-71-freq:workspace:/ keil add-source User/new.c --group User --reason TEXT --lease-token TOKEN", "device-pod-cli device-pod-71-freq:workspace:/ build start --reason TEXT --lease-token TOKEN", "device-pod-cli device-pod-71-freq:io-probe:/uart/1 jsonrpc gpio.read --params-json '{\"pin\":\"PB5\'} --reason TEXT --lease-token TOKEN", "device-pod-cli job output --pod-id device-pod-71-freq --api-base-url URL --session-token TOKEN", "device-pod-cli lease acquire --pod-id device-pod-71-freq --reason TEXT --api-base-url URL --session-token TOKEN" ] }); } async function setupCommand({ parsed, rest, env, fetchImpl }: any) { const subcommand = rest[0] || "first-admin"; if (subcommand !== "first-admin") throw cliError("unsupported_setup_command", `unsupported setup command: ${subcommand}`); const devicePod = devicePodSeedFromOptions(parsed, { required: false }); const body = clean({ username: requiredOption(parsed, env, "username", "HWLAB_USERNAME"), password: requiredOption(parsed, env, "password", "HWLAB_PASSWORD"), displayName: text(parsed.displayName), ...(devicePod ? { devicePod } : {}) }); const response = await requestJson({ parsed, env, fetchImpl, method: "POST", path: "/v1/setup/first-admin", body, auth: false }); return responsePayload("setup.first-admin", response, { setCookie: response.headers.get("set-cookie") ?? null, devicePodSeedProvided: Boolean(devicePod), localProfileAuthority: false }); } async function adminCommand(context: any) { const subcommand = context.rest[0] || "help"; if (subcommand === "device-pod" || subcommand === "profile") return adminDevicePodCommand({ ...context, rest: context.rest.slice(1) }); if (subcommand === "grant" || subcommand === "device-pod-grant") return adminGrantCommand({ ...context, rest: context.rest.slice(1) }); throw cliError("unsupported_admin_command", `unsupported admin command: ${subcommand}`); } async function adminDevicePodCommand({ parsed, rest, env, fetchImpl }: any) { const subcommand = rest[0] || "upsert"; if (!["create", "upsert", "update", "put"].includes(subcommand)) throw cliError("unsupported_admin_device_pod_command", `unsupported admin device-pod command: ${subcommand}`); const devicePod = devicePodSeedFromOptions(parsed, { required: true }); const method = subcommand === "update" || subcommand === "put" ? "PUT" : "POST"; const path = method === "PUT" ? `/v1/admin/device-pods/${encodeURIComponent(devicePod.devicePodId)}` : "/v1/admin/device-pods"; const response = await requestJson({ parsed, env, fetchImpl, method, path, body: devicePod }); return responsePayload(`admin.device-pod.${subcommand}`, response, { route: { method, path }, devicePodId: devicePod.devicePodId, localProfileAuthority: false }); } async function adminGrantCommand({ parsed, rest, env, fetchImpl }: any) { const podId = requiredText(parsed.podId ?? parsed.devicePodId ?? rest[0], "podId"); const userId = requiredText(parsed.userId ?? rest[1], "userId"); const body = { devicePodId: podId, userId }; const response = await requestJson({ parsed, env, fetchImpl, method: "POST", path: "/v1/admin/device-pod-grants", body }); return responsePayload("admin.device-pod-grant", response, { devicePodId: podId, userId }); } async function login({ parsed, env, fetchImpl }: any) { const response = await requestJson({ parsed, env, fetchImpl, method: "POST", path: "/auth/login", body: { username: requiredOption(parsed, env, "username", "HWLAB_USERNAME"), password: requiredOption(parsed, env, "password", "HWLAB_PASSWORD") }, auth: false }); return responsePayload("login", response, { setCookie: response.headers.get("set-cookie") ?? null }); } async function doctor({ parsed, env, fetchImpl }: any) { const podId = text(parsed.podId ?? parsed._[1]); const probes = []; for (const path of ["/v1/access/status", "/auth/session", "/v1/device-pods", podId ? `/v1/device-pods/${encodeURIComponent(podId)}/status` : ""].filter(Boolean)) { try { const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path }); probes.push({ path, httpStatus: response.status, ok: response.status < 400, body: compactBody(response.body) }); } catch (error) { probes.push({ path, ok: false, blocker: errorSummary(error) }); } } const hardFailed = probes.some((probe) => probe.ok === false && Number(probe.httpStatus ?? 0) >= 500); return ok("doctor", { status: hardFailed ? "degraded" : "succeeded", profileAuthority: "hwlab-cloud-api", localProfileAuthority: false, localProfileRead: false, devicePodId: podId || null, probes }, hardFailed ? "degraded" : "succeeded"); } async function health({ parsed, rest, env, fetchImpl }: any) { const podId = text(parsed.podId ?? rest[0]); const path = podId ? `/v1/device-pods/${encodeURIComponent(podId)}/status` : "/health/live"; const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path, auth: Boolean(podId) }); return responsePayload("health", response, { route: { method: "GET", path }, devicePodId: podId || null, localProfileAuthority: false }); } async function profileCommand({ parsed, rest, env, fetchImpl }: any) { const subcommand = rest[0] || "list"; if (subcommand === "create") { throw cliError("legacy_profile_create_removed", "local device-pod profile creation is removed from v0.2 formal mode; use cloud-api admin device-pod APIs", { next: ["POST /v1/admin/device-pods", "POST /v1/admin/device-pod-grants"] }); } if (subcommand === "list") { const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path: "/v1/device-pods" }); return responsePayload("profile.list", response, { localProfileAuthority: false, localProfileRead: false }); } if (subcommand === "show") { const podId = requiredText(parsed.podId ?? rest[1], "podId"); const path = `/v1/device-pods/${encodeURIComponent(podId)}/status`; const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path }); return responsePayload("profile.show", response, { route: { method: "GET", path }, devicePodId: podId, localProfileAuthority: false, localProfileRead: false }); } throw cliError("unsupported_profile_command", `unsupported profile command: ${subcommand}`); } async function leaseCommand({ parsed, rest, env, fetchImpl }: any) { const subcommand = rest[0] || "current"; const podId = requiredText(parsed.podId ?? rest[1], "podId"); const basePath = `/v1/device-pods/${encodeURIComponent(podId)}/leases`; if (subcommand === "acquire" || subcommand === "create") { const body = clean({ reason: text(parsed.reason), ttlSeconds: numberOption(parsed.ttlSeconds), agentSessionId: text(parsed.agentSessionId), projectId: text(parsed.projectId) }); const response = await requestJson({ parsed, env, fetchImpl, method: "POST", path: basePath, body }); return responsePayload("lease.acquire", response, { devicePodId: podId }); } if (subcommand === "current" || subcommand === "show") { const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path: `${basePath}/current` }); return responsePayload("lease.current", response, { devicePodId: podId }); } if (subcommand === "release" || subcommand === "delete") { const leaseToken = text(parsed.leaseToken ?? env.HWLAB_DEVICE_LEASE_TOKEN); const response = await requestJson({ parsed, env, fetchImpl, method: "DELETE", path: `${basePath}/current`, extraHeaders: leaseToken ? { "x-hwlab-device-lease-token": leaseToken } : {} }); return responsePayload("lease.release", response, { devicePodId: podId, leaseTokenProvided: Boolean(leaseToken) }); } throw cliError("unsupported_lease_command", `unsupported lease command: ${subcommand}`); } async function jobCommand({ parsed, rest, env, fetchImpl }: any) { const subcommand = rest[0] || "status"; const podId = requiredText(parsed.podId, "podId"); const jobId = requiredText(rest[1] ?? parsed.jobId, "jobId"); const suffix = subcommand === "output" ? "/output" : subcommand === "cancel" ? "/cancel" : ""; const method = subcommand === "cancel" ? "POST" : "GET"; const path = `/v1/device-pods/${encodeURIComponent(podId)}/jobs/${encodeURIComponent(jobId)}${suffix}`; const response = await requestJson({ parsed, env, fetchImpl, method, path }); return responsePayload(`job.${subcommand}`, response, { route: { method, path }, devicePodId: podId, jobId }); } async function invokeSelector(selector: any, context: any) { const { parsed, rest } = context; const operation = rest[0] || defaultOperation(selector.surface); const jobRoute = jobRouteFromSelector(selector, operation, rest, parsed); if (jobRoute) return jobCommand({ ...context, parsed: { ...parsed, podId: selector.podId }, rest: jobRoute }); if (selector.surface === "debug-probe" && operation === "status") return probeGet("debug.status", selector, "/status", context); const job = await buildJob(selector, operation, rest, parsed, context.stdinText); if (parsed.dryRun === true) return ok("device-pod.plan", { route: { method: "POST", path: `/v1/device-pods/${encodeURIComponent(selector.podId)}/jobs` }, request: redactJobRequest(job), selector, localProfileAuthority: false, localProfileRead: false }); const response = await requestJson({ parsed, env: context.env, fetchImpl: context.fetchImpl, method: "POST", path: `/v1/device-pods/${encodeURIComponent(selector.podId)}/jobs`, body: job }); return responsePayload("device-pod.invoke", response, { selector, intent: job.intent, mutating: MUTATING_INTENTS.has(job.intent), localProfileAuthority: false, localProfileRead: false }); } async function probeGet(action: string, selector: any, suffix: string, { parsed, env, fetchImpl }: any) { const path = `/v1/device-pods/${encodeURIComponent(selector.podId)}${suffix}`; const response = await requestJson({ parsed, env, fetchImpl, method: "GET", path }); return responsePayload(action, response, { route: { method: "GET", path }, selector, localProfileAuthority: false }); } async function buildJob(selector: any, operation: string, rest: string[], parsed: ParsedArgs, stdinText?: string) { const args = clean({}); let intent = ""; if (selector.surface === "workspace") { const basePath = selector.path || "."; if (operation === "ls") { intent = "workspace.ls"; args.path = joinPath(basePath, rest[1] || ""); } else if (operation === "cat") { intent = "workspace.cat"; args.path = joinPath(basePath, rest[1] || ""); args.maxBytes = numberOption(parsed.maxBytes ?? parsed.limit); } else if (operation === "rg") { intent = "workspace.rg"; args.pattern = requiredText(rest[1], "pattern"); args.path = joinPath(basePath, rest[2] || ""); Object.assign(args, passthroughOptions(parsed, [ "glob", "g", "filesWithMatches", "l", "ignoreCase", "i", "maxCount" ])); } else if (operation === "put") { intent = "workspace.put"; args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path")); Object.assign( args, await contentPayload(parsed, stdinText), passthroughOptions(parsed, ["encoding", "charset", "createOnly", "createDirs", "updateOnly"]) ); } else if (operation === "rm") { intent = "workspace.rm"; args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path")); Object.assign(args, passthroughOptions(parsed, ["missingOk"])); } else if (operation === "rmdir") { intent = "workspace.rmdir"; args.path = joinPath(basePath, requiredText(rest[1] ?? parsed.path, "path")); } else if (operation === "keil") { intent = "workspace.keil"; args.action = requiredText(rest[1], "keil action"); args.base = basePath; args.path = text(rest[2] ?? parsed.path ?? parsed.source ?? parsed.file ?? parsed.src); Object.assign(args, passthroughOptions(parsed, ["group", "groupName", "target", "timeoutMs"])); } else if (operation === "apply-patch") { intent = "workspace.apply-patch"; args.base = joinPath(basePath, rest[1] || ""); args.patch = await patchText(parsed, stdinText); Object.assign(args, passthroughOptions(parsed, ["encoding", "charset", "createDirs"])); } else if (operation === "build") { intent = "workspace.build"; args.action = rest[1] || "start"; Object.assign(args, passthroughOptions(parsed, ["target", "timeoutMs", "clean", "dryRun"])); } else throw cliError("unsupported_workspace_operation", `unsupported workspace operation: ${operation}`); } else if (selector.surface === "debug-probe") { if (operation === "chip-id") intent = "debug.chip-id"; else if (operation === "download") { intent = "debug.download"; args.action = rest[1] || "start"; Object.assign(args, passthroughOptions(parsed, [ "target", "timeoutMs", "captureUart", "captureDurationMs", "durationMs", "port", "baudRate" ])); } else if (operation === "reset") intent = "debug.reset"; else throw cliError("unsupported_debug_operation", `unsupported debug-probe operation: ${operation}`); } else if (selector.surface === "io-probe") { const uartId = normalizeProbePath(selector.path || "uart/1"); if (operation === "ports") { intent = "io.ports"; args.uartId = uartId; } else if (operation === "read") { intent = "io.uart.read"; args.uartId = uartId; Object.assign(args, passthroughOptions(parsed, ["durationMs", "port", "baudRate"])); } else if (operation === "read-after-launch-flash") { intent = "io.uart.read-after-launch-flash"; args.uartId = uartId; Object.assign(args, passthroughOptions(parsed, [ "durationMs", "port", "baudRate", "flashBase", "skipHardwareReset", "connectMode", "timeoutMs" ])); } else if (operation === "write") { intent = "io.uart.write"; args.uartId = uartId; args.message = text(rest[1] ?? parsed.message ?? parsed.text ?? parsed.data); args.hex = parsed.hex === true; Object.assign(args, passthroughOptions(parsed, ["port", "baudRate"])); } else if (operation === "jsonrpc") { intent = "io.uart.jsonrpc"; args.uartId = uartId; args.method = text(rest[1] ?? parsed.method); Object.assign(args, jsonRpcArgs(parsed), passthroughOptions(parsed, uartJsonRpcOptionKeys)); } else throw cliError("unsupported_io_operation", `unsupported io-probe operation: ${operation}`); } return clean({ intent, args: clean(args), reason: text(parsed.reason), leaseToken: text(parsed.leaseToken) }); } function jobRouteFromSelector(selector: any, operation: string, rest: string[], parsed: ParsedArgs) { if (operation === "status" || operation === "output" || operation === "cancel") return [operation, requiredText(rest[1] ?? parsed.jobId, "jobId")]; if ((selector.surface === "workspace" && operation === "build") || (selector.surface === "debug-probe" && operation === "download")) { if (["status", "output", "cancel"].includes(rest[1])) return [rest[1], requiredText(rest[2] ?? parsed.jobId, "jobId")]; } return null; } async function requestJson({ parsed, env, fetchImpl, method, path, body, auth = true, extraHeaders = {} }: any) { const baseUrl = apiBaseUrl(parsed, env); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS); try { const response = await fetchImpl(`${baseUrl}${path}`, { method, headers: clean({ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...(auth ? authHeaders(parsed, env) : {}), ...extraHeaders }), body: body ? JSON.stringify(body) : undefined, signal: controller.signal }); const textBody = await response.text(); return { status: response.status, headers: response.headers, body: parseJson(textBody) }; } finally { clearTimeout(timeout); } } function parseOptions(argv: string[]): ParsedArgs { const out: ParsedArgs = { _: [] }; for (let i = 0; i < argv.length; i += 1) { const item = argv[i] ?? ""; if (/^-[A-Za-z]$/u.test(item)) { const key = item.slice(1); const next = argv[i + 1]; if (next && !next.startsWith("-")) { setOption(out, key, next); i += 1; } else setOption(out, key, true); continue; } if (!item.startsWith("--")) { out._.push(item); continue; } const eq = item.indexOf("="); const rawKey = eq >= 0 ? item.slice(2, eq) : item.slice(2); const key = rawKey.replace(/-([a-z])/gu, (_, c) => String(c).toUpperCase()); if (eq >= 0) { setOption(out, key, item.slice(eq + 1)); continue; } const next = argv[i + 1]; if (next && !next.startsWith("--")) { setOption(out, key, next); i += 1; } else setOption(out, key, true); } return out; } function setOption(out: ParsedArgs, key: string, value: unknown) { if (out[key] === undefined) out[key] = value; else if (Array.isArray(out[key])) (out[key] as unknown[]).push(value); else out[key] = [out[key], value]; } function parseSelector(raw: string) { const match = raw.match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u); return match ? { podId: match[1], surface: match[2], path: match[2] === "io-probe" ? normalizeProbePath(match[3] || "") : normalizePath(match[3] || ".") } : null; } function passthroughOptions(parsed: ParsedArgs, keys: string[]) { const out: Record = {}; for (const key of keys) { const value = parsed[key]; if (value === undefined || value === false || value === "") continue; out[key] = typeof value === "string" && /^\d+$/u.test(value) ? Number.parseInt(value, 10) : value; } return out; } const MUTATING_INTENTS = new Set([ "workspace.apply-patch", "workspace.build", "workspace.put", "workspace.rm", "workspace.rmdir", "workspace.keil", "debug.download", "debug.reset", "io.uart.read-after-launch-flash", "io.uart.write", "io.uart.jsonrpc" ]); const uartJsonRpcOptionKeys = [ "id", "request", "requestB64", "params", "paramsB64", "responseTimeoutMs", "durationMs", "retry", "retries", "retryDelayMs", "lineEnding", "noNewline", "lineDelimited", "discardBefore", "requireResponse", "requireJson", "requireJsonrpc", "requireJsonrpcResult", "allowIdMismatch", "port", "baudRate" ]; function defaultOperation(surface: string) { return surface === "workspace" ? "ls" : surface === "debug-probe" ? "status" : "read"; } function apiBaseUrl(parsed: ParsedArgs, env: EnvLike) { const value = text(parsed.apiBaseUrl ?? parsed.apiUrl ?? env.HWLAB_DEVICE_POD_API_URL ?? env.HWLAB_CLOUD_API_URL); if (!value) throw cliError("api_base_url_required", "device-pod-cli requires --api-base-url or HWLAB_DEVICE_POD_API_URL/HWLAB_CLOUD_API_URL"); return value.replace(/\/+$/u, ""); } function authHeaders(parsed: ParsedArgs, env: EnvLike) { const cookie = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE); const sessionToken = text(parsed.sessionToken ?? env.HWLAB_DEVICE_POD_SESSION_TOKEN ?? env.HWLAB_CLOUD_API_SESSION_TOKEN ?? env.HWLAB_SESSION_TOKEN); const bearer = text(parsed.bearerToken ?? env.HWLAB_BEARER_TOKEN); return clean({ ...(cookie ? { cookie: cookie.includes("=") ? cookie : `hwlab_session=${encodeURIComponent(cookie)}` } : {}), ...(sessionToken ? { "x-hwlab-session-token": sessionToken } : {}), ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) }); } function responsePayload(action: string, response: any, extra: Record = {}) { const success = response.status >= 200 && response.status < 300 && response.body?.ok !== false; return { ok: success, action, status: success ? "succeeded" : "failed", httpStatus: response.status, ...extra, body: response.body }; } function withMeta(payload: any, now: () => string) { return { generatedAt: now(), cli: CLI_NAME, version: VERSION, ...payload }; } function ok(action: string, data: Record = {}, status = "succeeded") { return { ok: true, action, status, ...data }; } function failure(action: string, error: any) { const summary = errorSummary(error); return { ok: false, action, status: "failed", error: summary, ...patchHintForText(summary.message), ...(error?.details ? { details: error.details } : {}) }; } function cliError(code: string, message: string, details: Record = {}) { return Object.assign(new Error(message), { code, details }); } function errorSummary(error: any) { return { code: error?.code ?? "device_pod_cli_error", message: error?.message ?? String(error), ...(error?.details ? { details: error.details } : {}) }; } function requiredOption(parsed: ParsedArgs, env: EnvLike, key: string, envKey: string) { return requiredText(parsed[key] ?? env[envKey], key); } function requiredText(value: unknown, field: string) { const valueText = text(value); if (!valueText) throw cliError("missing_required_value", `${field} is required`, { field }); return valueText; } function text(value: unknown) { return String(value ?? "").trim(); } function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; } function clean>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== false)) as T; } function arrayOption(value: unknown) { return (Array.isArray(value) ? value : value === undefined ? [] : [value]).map((item) => text(item)).filter(Boolean); } function devicePodSeedFromOptions(parsed: ParsedArgs, { required = false } = {}) { const devicePodText = text(parsed.devicePodJson ?? parsed.devicePod); if (devicePodText) return normalizeDevicePodSeed(parseJsonObjectOption(devicePodText, "devicePodJson"), "devicePodJson"); const profileText = text(parsed.profileJson ?? parsed.profile); if (!profileText) { if (required) throw cliError("device_pod_profile_required", "--profile-json or --device-pod-json is required", { field: "profileJson" }); return null; } return normalizeDevicePodSeed({ devicePodId: parsed.podId ?? parsed.devicePodId, name: parsed.name, status: parsed.status, profile: parseJsonObjectOption(profileText, "profileJson") }, "devicePod"); } function normalizeDevicePodSeed(value: any, field: string) { if (!value || typeof value !== "object" || Array.isArray(value)) throw cliError("invalid_device_pod_seed", `${field} must be a JSON object`, { field }); const devicePodId = requiredText(value.devicePodId ?? value.id, `${field}.devicePodId`); const profile = value.profile ?? value.profileJson; if (!profile || typeof profile !== "object" || Array.isArray(profile)) throw cliError("invalid_device_pod_profile", `${field}.profile must be a JSON object`, { field: `${field}.profile` }); return clean({ devicePodId, name: text(value.name), status: text(value.status), profile }); } function parseJsonObjectOption(value: string, field: string) { try { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object"); return parsed; } catch (error: any) { throw cliError("invalid_json_option", `${field} must be a JSON object`, { field, reason: error?.message ?? String(error) }); } } function normalizePath(value: string) { return String(value || ".").trim().replace(/\\/gu, "/").replace(/\s*\/\s*/gu, "/").replace(/\/+/gu, "/").replace(/^\/+|\/+$/gu, "") || "."; } function normalizeProbePath(value: string) { const normalized = normalizePath(value); if (normalized === ".") return "uart/1"; if (normalized === "uart") throw cliError("invalid_io_probe_path", "io-probe path must be concrete, for example /uart/1"); return normalized; } function joinPath(base: string, child: string) { const left = normalizePath(base); const right = normalizePath(child || "."); if (!right || right === ".") return left; if (child.startsWith("/")) return right; return normalizePath(`${left}/${right}`); } async function patchText(parsed: ParsedArgs, stdinText?: string) { if (typeof parsed.patch === "string") return parsed.patch; if (typeof parsed.patchB64 === "string") return Buffer.from(parsed.patchB64, "base64").toString("utf8"); if (stdinText !== undefined) return stdinText; const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); const textValue = Buffer.concat(chunks).toString("utf8"); if (!textValue.trim()) throw cliError("patch_text_required", "workspace apply-patch requires patch text on stdin or --patch/--patch-b64"); return textValue; } async function contentPayload(parsed: ParsedArgs, stdinText?: string) { if (typeof parsed.contentB64 === "string") return { contentB64: parsed.contentB64 }; if (typeof parsed.textB64 === "string") return { textB64: parsed.textB64 }; if (typeof parsed.text === "string" || typeof parsed.content === "string") { return { text: String(parsed.text ?? parsed.content) }; } const content = stdinText !== undefined ? stdinText : await readStdinText(); if (!content) { throw cliError( "content_required", "workspace put requires stdin text or --content-b64/--text/--text-b64" ); } return { contentB64: Buffer.from(content, "utf8").toString("base64") }; } async function readStdinText() { const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8"); } function jsonRpcArgs(parsed: ParsedArgs) { return clean({ request: parsed.requestJson, params: parsed.paramsJson ?? parsed.params, expectResultField: arrayOption(parsed.expectResultField ?? parsed.resultField) }); } function redactJobRequest(job: any) { return { ...job, args: redactArgs(job.args) }; } function redactArgs(args: any = {}) { if (!args || typeof args !== "object") return args; const next = { ...args }; if (typeof next.patch === "string") { next.patch = undefined; next.patchBytes = Buffer.byteLength(args.patch, "utf8"); } if (typeof next.contentB64 === "string") { next.contentB64 = undefined; next.contentBytes = Buffer.from(args.contentB64, "base64").length; } if (typeof next.text === "string") { next.text = undefined; next.textBytes = Buffer.byteLength(args.text, "utf8"); } return next; } function patchHintForText(message: unknown) { const textValue = String(message ?? ""); const patchLike = /apply-patch|patch|hunk|\*\*\* Begin Patch|\*\*\* End Patch|Update File|Add File|Delete File/iu; if (!patchLike.test(textValue)) return {}; return { patchHint: { standardForm: [ "*** Begin Patch", "*** Update File: path/to/file.c", "@@", " exact unchanged context line", "-old line", "+new line", "*** End Patch" ], next: patchHintNext(textValue) } }; } function patchHintNext(message: string) { const next = [ "Re-read the current target file with workspace cat/rg, then retry a smaller exact-context hunk patch." ]; if (/end with \*\*\* End Patch/iu.test(message)) { next.unshift("Add a final exact `*** End Patch` line."); } else if (/start with \*\*\* Begin Patch/iu.test(message)) { next.unshift("Start the patch with an exact `*** Begin Patch` line."); } else if (/hunk did not match|must start with @@|invalid update line prefix|ellipsis/iu.test(message)) { next.unshift("Use exact current context; do not use ellipsis or line-number-only hunks."); } else if (/unsupported patch header|capitalization|New File/iu.test(message)) { next.unshift("Use exactly `*** Update File:`, `*** Add File:`, or `*** Delete File:` headers."); } next.push("Do not switch to workspace put for existing source unless a smaller hunk cannot safely express the edit."); return next; } function parseJson(value: string) { if (!value) return null; try { return JSON.parse(value); } catch { return { rawText: value.slice(0, 2000), parseError: true }; } } function compactBody(body: any) { if (!body || typeof body !== "object") return body; return { ok: body.ok, status: body.status, authenticated: body.authenticated, actor: body.actor, contractVersion: body.contractVersion, devicePodCount: Array.isArray(body.devicePods) ? body.devicePods.length : undefined, selectedDevicePodId: body.selectedDevicePodId, error: body.error, summary: body.summary }; }