Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-docs-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typesensekit/cli": patch
---

Add operation-level `--schema` and `--examples` helpers for discovering command input shapes.
50 changes: 50 additions & 0 deletions packages/cli/src/operation-docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { operations } from "@typesensekit/core";
import { describe, expect, it } from "vitest";
import {
renderInputSchema,
renderOperationExamples,
} from "./operation-docs.js";

function operationInput(name: string) {
const operation = operations.find((candidate) => candidate.name === name);
if (!operation) throw new Error(`${name} not found`);
return operation.input;
}

describe("operation docs", () => {
it("renders the top-level input shape for documents.search", () => {
expect(
JSON.parse(renderInputSchema(operationInput("documents.search"))),
).toEqual({
type: "object",
properties: {
collection: { type: "string" },
params: {
type: "object",
additionalProperties: {
anyOf: [
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
{ type: "array", items: { type: "string" } },
{ type: "array", items: { type: "number" } },
],
},
},
},
required: ["collection", "params"],
});
});

it("renders command-specific examples for common operations", () => {
expect(renderOperationExamples("documents.search")).toContain(
`tsk documents.search --input '{"collection":"products","params":{"q":"chair","query_by":"title,description"}}' --json`,
);
expect(renderOperationExamples("presets.create")).toContain(
`tsk presets.create --input '{"name":"Semantic","value":{"query_by":"title_embedding"}}' --json`,
);
expect(renderOperationExamples("api.call")).toContain(
`tsk api.call --input '{"method":"get","path":"/collections"}' --json`,
);
});
});
186 changes: 186 additions & 0 deletions packages/cli/src/operation-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { z } from "zod";

type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };

type JsonSchema = {
type?: string;
enum?: string[];
anyOf?: JsonSchema[];
items?: JsonSchema;
additionalProperties?: JsonSchema | boolean;
properties?: Record<string, JsonSchema>;
required?: string[];
};

const EXAMPLES: Record<string, JsonValue[]> = {
"api.call": [
{
method: "get",
path: "/collections",
},
{
method: "put",
path: "/presets/Semantic",
body: { value: { query_by: "title_embedding" } },
},
],
"collections.create": [
{
name: "products",
fields: [
{ name: "title", type: "string" },
{ name: "price", type: "float", optional: true },
],
},
],
"collections.fields.add": [
{
collection: "products",
field: "title_embedding",
type: "float[]",
numDim: 1536,
embedFrom: "title",
embedModel: "openai/text-embedding-3-small",
},
],
"collections.wait": [
{
collection: "products",
fieldPresent: "title_embedding",
timeoutMs: 60000,
},
],
"documents.index": [
{
collection: "products",
document: { id: "sku-1", title: "Lounge chair" },
},
],
"documents.search": [
{
collection: "products",
params: {
q: "chair",
query_by: "title,description",
},
},
],
"keys.create": [
{
description: "Search-only key",
actions: ["documents:search"],
collections: ["products"],
},
],
"presets.create": [
{
name: "Semantic",
value: { query_by: "title_embedding" },
},
],
"synonyms.create": [
{
collection: "products",
id: "sofa-couch",
synonyms: ["sofa", "couch"],
},
],
};

function unwrapSchema(input: z.ZodTypeAny): {
schema: z.ZodTypeAny;
optional: boolean;
} {
if (input instanceof z.ZodOptional) {
const inner = unwrapSchema(input.unwrap());
return { schema: inner.schema, optional: true };
}
if (input instanceof z.ZodDefault) {
const inner = unwrapSchema(input.removeDefault());
return { schema: inner.schema, optional: true };
}
if (input instanceof z.ZodNullable) {
return unwrapSchema(input.unwrap());
}
return { schema: input, optional: false };
}

function describeSchema(input: z.ZodTypeAny): JsonSchema {
const { schema } = unwrapSchema(input);

if (schema instanceof z.ZodString) return { type: "string" };
if (schema instanceof z.ZodNumber) return { type: "number" };
if (schema instanceof z.ZodBoolean) return { type: "boolean" };
if (schema instanceof z.ZodEnum)
return { type: "string", enum: schema.options };
if (schema instanceof z.ZodArray) {
return { type: "array", items: describeSchema(schema.element) };
}
if (schema instanceof z.ZodRecord) {
const valueType = (schema as z.ZodRecord<z.ZodTypeAny, z.ZodTypeAny>)
.valueSchema;
return {
type: "object",
additionalProperties: valueType ? describeSchema(valueType) : true,
};
}
if (schema instanceof z.ZodUnion) {
const options = schema.options as z.ZodTypeAny[];
return {
anyOf: options.map((option) => describeSchema(option)),
};
}
if (schema instanceof z.ZodObject) {
return inputObjectSchema(schema);
}

return { type: "unknown" };
}

export function inputObjectSchema(input: z.ZodTypeAny): JsonSchema {
const { schema } = unwrapSchema(input);
if (!(schema instanceof z.ZodObject)) return describeSchema(schema);

const properties: Record<string, JsonSchema> = {};
const required: string[] = [];

const shape = schema.shape as z.ZodRawShape;
for (const [name, child] of Object.entries(shape)) {
const { optional } = unwrapSchema(child);
properties[name] = describeSchema(child);
if (!optional) required.push(name);
}

return {
type: "object",
properties,
required,
};
}

export function renderInputSchema(input: z.ZodTypeAny): string {
return JSON.stringify(inputObjectSchema(input), null, 2);
}

export function renderOperationExamples(operationName: string): string {
const examples = EXAMPLES[operationName];
if (!examples) {
return [
`No curated examples are available for ${operationName}.`,
`Run: tsk ${operationName} --schema`,
].join("\n");
}

return examples
.map(
(example) =>
`tsk ${operationName} --input '${JSON.stringify(example)}' --json`,
)
.join("\n");
}
21 changes: 21 additions & 0 deletions packages/cli/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
operations,
} from "@typesensekit/core";
import { defineCommand } from "citty";
import {
renderInputSchema,
renderOperationExamples,
} from "./operation-docs.js";
import { parseInput, render } from "./output.js";
import { resolveClient } from "./profile/resolve.js";

Expand Down Expand Up @@ -97,6 +101,14 @@ export function operationCommands() {
type: "string",
description: "JSON object matching this operation input schema",
},
schema: {
type: "boolean",
description: "Print this operation input schema",
},
examples: {
type: "boolean",
description: "Print example invocations for this operation",
},
profile: { type: "string", description: "Profile name" },
config: { type: "string", description: "Profile config path" },
json: { type: "boolean", description: "Print JSON" },
Expand All @@ -107,6 +119,15 @@ export function operationCommands() {
...operationSpecificArgs(operation.name),
},
async run({ args }) {
if (args.schema) {
console.log(renderInputSchema(operation.input));
return;
}
if (args.examples) {
console.log(renderOperationExamples(operation.name));
return;
}

const client = await resolveClient({
profile: args.profile,
config: args.config,
Expand Down
Loading