Compare commits

..

4 Commits

Author SHA1 Message Date
xiaoju 7dd6ab5328 feat: --format json/yaml/table for all non-interactive commands
Add program-level --format option (default: json) inherited by all
subcommands. json output unchanged, yaml via yaml package, table
renders aligned columns for arrays, falls back to yaml for objects.

Closes #328

小橘 🍊(NEKO Team)
2026-05-18 13:33:41 +00:00
xiaomo 7c955fa749 Merge pull request 'fix: uwf cas — JSON output + meta-schema in schema list' (#326) from fix/319-cas-json-output into main 2026-05-18 13:25:16 +00:00
xiaoju c4c9f96117 fix: uwf cas commands output JSON, include meta-schema in schema list
All cas subcommands now output JSON via writeJson(), consistent with
other uwf commands. schema list includes meta-schema. Removed --json
flag and --format tree (tree is human-only, not machine-friendly).

Refs #319

小橘 🍊(NEKO Team)
2026-05-18 13:24:19 +00:00
xiaomo 633d5aeafe Merge pull request 'refactor: outputSchema only accepts inline JSON Schema' (#325) from fix/319-validate-schema-only-inline into main 2026-05-18 13:18:17 +00:00
3 changed files with 120 additions and 100 deletions
+45 -31
View File
@@ -22,9 +22,11 @@ import {
cmdCasWalk,
} from "./commands/cas.js";
import { resolveStorageRoot } from "./store.js";
import { type OutputFormat, formatOutput } from "./format.js";
function writeJson(data: unknown): void {
process.stdout.write(`${JSON.stringify(data)}\n`);
function writeOutput(data: unknown): void {
const fmt = program.opts().format as OutputFormat;
process.stdout.write(`${formatOutput(data, fmt)}\n`);
}
function runAction(action: () => Promise<void>): void {
@@ -38,6 +40,7 @@ function runAction(action: () => Promise<void>): void {
const program = new Command();
program.name("uwf").description("Stateless workflow CLI");
program.option("--format <fmt>", "Output format: json, yaml, table", "json");
const workflow = program.command("workflow").description("Workflow registry and CAS");
@@ -49,7 +52,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowPut(storageRoot, file);
writeJson(result);
writeOutput(result);
});
});
@@ -61,7 +64,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowShow(storageRoot, id);
writeJson(result);
writeOutput(result);
});
});
@@ -72,7 +75,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowList(storageRoot);
writeJson(result);
writeOutput(result);
});
});
@@ -87,7 +90,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadStart(storageRoot, workflow, opts.prompt);
writeJson(result);
writeOutput(result);
});
});
@@ -101,7 +104,7 @@ thread
runAction(async () => {
const agentOverride = opts.agent ?? null;
const result = await cmdThreadStep(storageRoot, threadId, agentOverride);
writeJson(result);
writeOutput(result);
});
});
@@ -113,7 +116,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadShow(storageRoot, threadId);
writeJson(result);
writeOutput(result);
});
});
@@ -125,7 +128,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadList(storageRoot, opts.all);
writeJson(result);
writeOutput(result);
});
});
@@ -137,7 +140,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadKill(storageRoot, threadId);
writeJson(result);
writeOutput(result);
});
});
@@ -167,7 +170,7 @@ program
agent: opts.agent ?? undefined,
storageRoot,
});
writeJson(result);
writeOutput(result);
} else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) {
await cmdSetupInteractive(storageRoot);
} else {
@@ -184,10 +187,11 @@ cas
.command("get")
.description("Read a CAS node as JSON")
.argument("<hash>", "CAS hash (13 char)")
.option("--json", "Compact JSON output")
.action((hash: string, opts: { json?: boolean }) => {
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasGet(storageRoot, hash, opts));
runAction(async () => {
writeOutput(await cmdCasGet(storageRoot, hash));
});
});
cas
@@ -195,10 +199,11 @@ cas
.description("Output a CAS node (--payload for payload only)")
.argument("<hash>", "CAS hash (13 char)")
.option("--payload", "Output only the payload")
.option("--json", "Compact JSON output")
.action((hash: string, opts: { payload?: boolean; json?: boolean }) => {
.action((hash: string, opts: { payload?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasCat(storageRoot, hash, opts));
runAction(async () => {
writeOutput(await cmdCasCat(storageRoot, hash, opts));
});
});
cas
@@ -206,19 +211,22 @@ cas
.description("Store a node, print its hash")
.argument("<type-hash>", "Type (schema) hash")
.argument("<data>", "JSON file path or inline JSON string")
.option("--json", "Compact JSON output")
.action((typeHash: string, data: string, opts: { json?: boolean }) => {
.action((typeHash: string, data: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasPut(storageRoot, typeHash, data, opts));
runAction(async () => {
writeOutput(await cmdCasPut(storageRoot, typeHash, data));
});
});
cas
.command("has")
.description("Check if a hash exists (prints true/false)")
.description("Check if a hash exists")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasHas(storageRoot, hash));
runAction(async () => {
writeOutput(await cmdCasHas(storageRoot, hash));
});
});
cas
@@ -227,37 +235,43 @@ cas
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasRefs(storageRoot, hash));
runAction(async () => {
writeOutput(await cmdCasRefs(storageRoot, hash));
});
});
cas
.command("walk")
.description("Recursive traversal from a node")
.argument("<hash>", "CAS hash (13 char)")
.option("--format <fmt>", "Output format: flat (default) or tree")
.action((hash: string, opts: { format?: string }) => {
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasWalk(storageRoot, hash, opts));
runAction(async () => {
writeOutput(await cmdCasWalk(storageRoot, hash));
});
});
const casSchema = cas.command("schema").description("CAS schema operations");
casSchema
.command("list")
.description("List all registered schemas (hash + name)")
.description("List all registered schemas")
.action(() => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasSchemaList(storageRoot));
runAction(async () => {
writeOutput(await cmdCasSchemaList(storageRoot));
});
});
casSchema
.command("get")
.description("Show a schema by its type hash")
.argument("<hash>", "Schema type hash")
.option("--json", "Compact JSON output")
.action((hash: string, opts: { json?: boolean }) => {
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(() => cmdCasSchemaGet(storageRoot, hash, opts));
runAction(async () => {
writeOutput(await cmdCasSchemaGet(storageRoot, hash));
});
});
program.parseAsync(process.argv).catch((e: unknown) => {
+40 -69
View File
@@ -11,12 +11,7 @@ function openStore(storageRoot: string): Store {
return createFsStore(join(storageRoot, "cas"));
}
function out(data: unknown, compact = false): void {
console.log(compact ? JSON.stringify(data) : JSON.stringify(data, null, 2));
}
function readJsonArg(fileOrInline: string): unknown {
// Try as inline JSON first, then as file path
try {
return JSON.parse(fileOrInline);
} catch {
@@ -28,138 +23,114 @@ function readJsonArg(fileOrInline: string): unknown {
}
}
// ---- Commands ----
// ---- Commands (all return JSON-serializable data) ----
export async function cmdCasGet(
storageRoot: string,
hash: string,
opts: { json?: boolean },
): Promise<void> {
): Promise<unknown> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
out(node, opts.json);
return node;
}
export async function cmdCasCat(
storageRoot: string,
hash: string,
opts: { payload?: boolean; json?: boolean },
): Promise<void> {
opts: { payload?: boolean },
): Promise<unknown> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
out(opts.payload ? node.payload : node, opts.json);
return opts.payload ? node.payload : node;
}
export async function cmdCasPut(
storageRoot: string,
typeHash: string,
data: string,
opts: { json?: boolean },
): Promise<void> {
): Promise<{ hash: string }> {
const store = openStore(storageRoot);
const payload = readJsonArg(data);
const hash = store.put(typeHash, payload);
console.log(hash);
const hash = await store.put(typeHash, payload);
return { hash };
}
export async function cmdCasHas(
storageRoot: string,
hash: string,
): Promise<void> {
): Promise<{ exists: boolean }> {
const store = openStore(storageRoot);
console.log(String(store.has(hash)));
return { exists: store.has(hash) };
}
export async function cmdCasList(storageRoot: string): Promise<void> {
const store = openStore(storageRoot);
for (const hash of store.list()) {
console.log(hash);
}
}
export async function cmdCasRefs(storageRoot: string, hash: string): Promise<void> {
export async function cmdCasRefs(
storageRoot: string,
hash: string,
): Promise<{ refs: string[] }> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
const refHashes = refs(store, node);
for (const r of refHashes) {
console.log(r);
}
return { refs: refs(store, node) };
}
export async function cmdCasWalk(
storageRoot: string,
hash: string,
opts: { format?: string },
): Promise<void> {
): Promise<{ hashes: string[] }> {
const store = openStore(storageRoot);
if (opts.format === "tree") {
const childMap = new Map<Hash, Hash[]>();
walk(store, hash, (h, node) => {
childMap.set(h, refs(store, node));
});
const printed = new Set<Hash>();
function printNode(h: Hash, prefix: string, isLast: boolean): void {
const connector = prefix === "" ? "" : isLast ? "└── " : "├── ";
if (printed.has(h)) {
console.log(`${prefix}${connector}${h} (seen)`);
return;
}
printed.add(h);
console.log(`${prefix}${connector}${h}`);
const kids = childMap.get(h) ?? [];
const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "│ ");
for (let i = 0; i < kids.length; i++) {
printNode(kids[i] as Hash, childPrefix, i === kids.length - 1);
}
}
printNode(hash, "", true);
} else {
walk(store, hash, (h) => {
console.log(h);
});
}
const result: string[] = [];
walk(store, hash, (h) => {
result.push(h);
});
return { hashes: result };
}
export async function cmdCasSchemaList(storageRoot: string): Promise<void> {
export type SchemaListEntry = {
hash: string;
title: string;
};
export async function cmdCasSchemaList(
storageRoot: string,
): Promise<SchemaListEntry[]> {
const store = openStore(storageRoot);
const metaHash = await bootstrap(store);
const entries: SchemaListEntry[] = [];
// Include meta-schema itself
entries.push({ hash: metaHash, title: "(meta-schema)" });
for (const hash of store.list()) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null && node.type === metaHash) {
const schema = node.payload as JSONSchema;
const name =
const title =
(schema.title as string | undefined) ??
(schema.description as string | undefined) ??
"(unnamed)";
console.log(`${hash} ${name}`);
entries.push({ hash, title });
}
}
return entries;
}
export async function cmdCasSchemaGet(
storageRoot: string,
hash: string,
opts: { json?: boolean },
): Promise<void> {
): Promise<unknown> {
const store = openStore(storageRoot);
const schema = getSchema(store, hash);
if (schema === null) {
throw new Error(`Schema not found: ${hash}`);
}
out(schema, opts.json);
return schema;
}
+35
View File
@@ -0,0 +1,35 @@
import { stringify } from "yaml";
export type OutputFormat = "json" | "yaml" | "table";
function formatTable(data: Array<Record<string, unknown>>): string {
if (data.length === 0) return "";
const keys = Object.keys(data[0]);
const widths = keys.map((k) => {
let max = k.length;
for (const row of data) {
const len = String(row[k] ?? "").length;
if (len > max) max = len;
}
return max;
});
const header = keys.map((k, i) => k.toUpperCase().padEnd(widths[i])).join(" ");
const rows = data.map((row) =>
keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i])).join(" "),
);
return [header, ...rows].join("\n");
}
export function formatOutput(data: unknown, format: OutputFormat): string {
switch (format) {
case "json":
return JSON.stringify(data);
case "yaml":
return stringify(data).trimEnd();
case "table":
if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null) {
return formatTable(data as Array<Record<string, unknown>>);
}
return stringify(data).trimEnd();
}
}