spawn/packages/shared/src/parse.ts
A f7c23de716
feat: add downloadFile to CloudRunner + local OpenClaw config merge (#2636)
* feat: add downloadFile to CloudRunner + local OpenClaw config merge

Add `downloadFile(remotePath, localPath)` to the CloudRunner interface
and implement it across all 6 cloud providers (Hetzner, AWS, GCP,
DigitalOcean, Sprite, Local) — mirroring the existing `uploadFile` with
reversed SCP direction.

Replace the OpenClaw config write with a download → deep-merge → upload
flow so config merging happens in our own linted TypeScript instead of
a remote script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move isPlainObject and deepMerge to shared utils

Extract `isPlainObject` to `shared/type-guards.ts` and `deepMerge` to
`shared/parse.ts` so they're reusable across the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: promote isPlainObject to shared package, use across codebase

Move `isPlainObject` from cli/type-guards.ts into
@openrouter/spawn-shared so it can be used everywhere. Replace
inline `val !== null && typeof val === "object" && !Array.isArray(val)`
checks in:

- shared/type-guards.ts (toRecord, toObjectArray)
- shared/parse.ts (parseJsonObj)
- cli/manifest.ts (isValidManifest)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove type-guards re-export, import directly from spawn-shared

Delete `packages/cli/src/shared/type-guards.ts` (was just a re-export
barrel). All 35 consuming files now import `getErrorMessage`, `isString`,
`isNumber`, `isPlainObject`, `toRecord`, etc. directly from
`@openrouter/spawn-shared`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:47:32 -07:00

37 lines
1 KiB
TypeScript

// shared/parse.ts — Schema-validated JSON parsing (replaces unsafe `as` casts)
// biome-ignore-all lint/plugin: parse implementations require raw try/catch around JSON.parse
import * as v from "valibot";
import { isPlainObject } from "./type-guards";
/**
* Parse a JSON string and validate it against a valibot schema.
* Returns the validated value, or null if parsing/validation fails.
*/
export function parseJsonWith<T extends v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>>(
text: string,
schema: T,
): v.InferOutput<T> | null {
try {
return v.parse(schema, JSON.parse(text));
} catch {
return null;
}
}
/**
* Parse a JSON string and return it as a Record<string, unknown> or null.
* Rejects non-object results (arrays, primitives).
* Use for API responses that are always a JSON object.
*/
export function parseJsonObj(text: string): Record<string, unknown> | null {
try {
const val: unknown = JSON.parse(text);
if (isPlainObject(val)) {
return val;
}
return null;
} catch {
return null;
}
}