refactor: address review feedback for CLI restructure

- Extract shared module (shared.ts) — walkChain, expandDeep, etc. deduplicated
- Hide step read command (half-baked, not ready for users)
- Remove cmdThreadKill dead code
- Revert unrelated protocol type change
- Revert unrelated package.json change
- Fix unused imports (biome)

Refs #463
This commit is contained in:
2026-05-24 11:32:47 +00:00
parent 650313b1c2
commit 669af841e1
6 changed files with 250 additions and 465 deletions
+10 -211
View File
@@ -1,9 +1,6 @@
import type { Store as CasStore, JSONSchema } from "@uncaged/json-cas";
import { getSchema } from "@uncaged/json-cas";
import type {
CasRef,
StartEntry,
StartNodePayload,
StepEntry,
StepNodePayload,
ThreadForkOutput,
@@ -11,213 +8,15 @@ import type {
ThreadStepsOutput,
} from "@uncaged/workflow-protocol";
import { generateUlid } from "@uncaged/workflow-util";
import { createUwfStore, loadThreadsIndex, saveThreadsIndex, type UwfStore } from "../store.js";
function fail(message: string): never {
process.stderr.write(`${message}\n`);
process.exit(1);
}
type ChainState = {
startHash: CasRef;
start: StartNodePayload;
stepsNewestFirst: StepNodePayload[];
headIsStart: boolean;
};
type OrderedStepItem = {
hash: CasRef;
payload: StepNodePayload;
timestamp: number;
};
function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
const headNode = uwf.store.get(headHash);
if (headNode === null) {
fail(`CAS node not found: ${headHash}`);
}
if (headNode.type === uwf.schemas.startNode) {
return {
startHash: headHash,
start: headNode.payload as StartNodePayload,
stepsNewestFirst: [],
headIsStart: true,
};
}
if (headNode.type !== uwf.schemas.stepNode) {
fail(`head ${headHash} is not a StartNode or StepNode`);
}
const stepsNewestFirst: StepNodePayload[] = [];
let hash: CasRef | null = headHash;
while (hash !== null) {
const node = uwf.store.get(hash);
if (node === null) {
fail(`CAS node not found while walking chain: ${hash}`);
}
if (node.type !== uwf.schemas.stepNode) {
break;
}
const payload = node.payload as StepNodePayload;
stepsNewestFirst.push(payload);
hash = payload.prev;
}
const newest = stepsNewestFirst[0];
if (newest === undefined) {
fail(`empty step chain at head ${headHash}`);
}
const startNode = uwf.store.get(newest.start);
if (startNode === null || startNode.type !== uwf.schemas.startNode) {
fail(`StartNode not found: ${newest.start}`);
}
return {
startHash: newest.start,
start: startNode.payload as StartNodePayload,
stepsNewestFirst,
headIsStart: false,
};
}
function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown {
const node = uwf.store.get(outputRef);
if (node === null) {
return {};
}
return node.payload;
}
/**
* Recursively expand all cas_ref fields in a CAS node's payload,
* replacing hash strings with the referenced node's expanded payload.
*/
function expandDeep(store: CasStore, hash: CasRef, visited?: Set<string>): unknown {
const seen = visited ?? new Set<string>();
if (seen.has(hash)) return hash; // cycle guard
seen.add(hash);
const node = store.get(hash);
if (node === null) return hash;
const schema = getSchema(store, node.type);
if (schema === null) return node.payload;
return expandValue(store, schema, node.payload, seen);
}
function expandCasRefField(store: CasStore, value: unknown, visited: Set<string>): unknown {
if (typeof value === "string") {
return expandDeep(store, value as CasRef, visited);
}
return value;
}
function expandAnyOfField(
store: CasStore,
schema: JSONSchema,
value: unknown,
visited: Set<string>,
): unknown {
if (!Array.isArray(schema.anyOf)) return value;
for (const sub of schema.anyOf as JSONSchema[]) {
if (sub.format === "cas_ref" && typeof value === "string") {
return expandDeep(store, value as CasRef, visited);
}
}
return value;
}
function expandArrayField(
store: CasStore,
schema: JSONSchema,
value: unknown,
visited: Set<string>,
): unknown {
if (!Array.isArray(value)) return value;
const itemSchema = schema.items as JSONSchema | undefined;
if (itemSchema === undefined) return value;
return value.map((item) => expandValue(store, itemSchema, item, visited));
}
function expandObjectField(
store: CasStore,
schema: JSONSchema,
value: unknown,
visited: Set<string>,
): unknown {
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
const props = schema.properties as Record<string, JSONSchema> | undefined;
if (props === undefined) return value;
const result: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value)) {
const propSchema = props[key];
result[key] = propSchema !== undefined ? expandValue(store, propSchema, val, visited) : val;
}
return result;
}
function expandValue(
store: CasStore,
schema: JSONSchema,
value: unknown,
visited: Set<string>,
): unknown {
if (schema.format === "cas_ref") {
return expandCasRefField(store, value, visited);
}
if (schema.anyOf !== undefined) {
return expandAnyOfField(store, schema, value, visited);
}
if (schema.type === "array") {
return expandArrayField(store, schema, value, visited);
}
if (schema.type === "object") {
return expandObjectField(store, schema, value, visited);
}
return value;
}
function collectOrderedSteps(
uwf: UwfStore,
headHash: CasRef,
chain: ChainState,
): OrderedStepItem[] {
const reversed = chain.stepsNewestFirst.slice().reverse();
const ordered: OrderedStepItem[] = [];
let hash: CasRef | null = chain.headIsStart ? null : headHash;
for (const payload of reversed) {
if (hash === null) {
fail("unexpected null hash while collecting ordered steps");
}
const node = uwf.store.get(hash);
if (node === null) {
fail(`CAS node not found: ${hash}`);
}
ordered.push({
hash,
payload,
timestamp: node.timestamp,
});
hash = payload.prev;
}
return ordered;
}
async function resolveHeadHash(storageRoot: string, threadId: ThreadId): Promise<CasRef> {
const index = await loadThreadsIndex(storageRoot);
const head = index[threadId];
if (head === undefined) {
fail(`thread not active: ${threadId}`);
}
return head;
}
import { createUwfStore, loadThreadsIndex, saveThreadsIndex } from "../store.js";
import {
collectOrderedSteps,
expandDeep,
expandOutput,
fail,
resolveHeadHash,
walkChain,
} from "./shared.js";
/**
* List all steps in a thread (previously: thread steps)
@@ -250,7 +49,7 @@ export async function cmdStepList(
hash: item.hash,
role: item.payload.role,
output: expandOutput(uwf, item.payload.output),
detail: item.payload.detail ? expandDeep(uwf.store, item.payload.detail) : null,
detail: item.payload.detail ?? null,
agent: item.payload.agent,
timestamp: item.timestamp,
});