Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dd6ab5328 | |||
| 7c955fa749 | |||
| c4c9f96117 | |||
| 633d5aeafe |
+45
-31
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user