From 31bf1ca6c94565351a92d2316858aa626a709d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 1 Jun 2026 08:32:06 +0000 Subject: [PATCH] feat: add --render / -r flag for inline render output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds global --render / -r flag that renders the JSON envelope output inline, equivalent to piping through `ocas render -p`. Works on all commands except render and help. Fixes #12 小橘 🍊(NEKO Team) --- packages/cli/src/index.ts | 68 ++++++++++++------- .../__snapshots__/edge-cases.test.ts.snap | 1 + 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 973e4ed..f823ff1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -77,6 +77,8 @@ function parseArgs(argv: string[]): { flags: Flags; positional: string[] } { } } else if (arg === "-p") { flags.p = true; + } else if (arg === "-r") { + flags.r = true; } else { positional.push(arg); } @@ -91,7 +93,7 @@ const defaultStorePath = join(homedir(), ".ocas"); const storePath = typeof flags.home === "string" ? flags.home - : (process.env["OCAS_HOME"] ?? defaultStorePath); + : (process.env.OCAS_HOME ?? defaultStorePath); const compact = flags.json === true; const defaultVarDbPath = join(storePath, "variables.db"); @@ -100,7 +102,22 @@ const varDbPath = // ---- Helpers ---- -function out(data: unknown): void { +const inlineRender = flags.render === true || flags.r === true; + +async function out(data: unknown): Promise { + if ( + inlineRender && + typeof data === "object" && + data !== null && + "type" in data && + "value" in data + ) { + const envelope = data as { type: string; value: unknown }; + const store = await openStore(); + const output = renderDirect(envelope.type as Hash, envelope.value, store, null); + process.stdout.write(output); + return; + } console.log(compact ? JSON.stringify(data) : JSON.stringify(data, null, 2)); } @@ -220,7 +237,7 @@ async function cmdPut(args: string[]): Promise { if (typeHash === metaHash) { try { const hash = await putSchema(store, payload as Record); - out(await wrapEnvelope(store, "@ocas/output/put", hash)); + await out(await wrapEnvelope(store, "@ocas/output/put", hash)); } catch (_e) { console.error( `Validation failed: payload in ${file} does not match schema ${typeHash}`, @@ -247,7 +264,7 @@ async function cmdPut(args: string[]): Promise { } const hash = await store.put(typeHash, payload); - out(await wrapEnvelope(store, "@ocas/output/put", hash)); + await out(await wrapEnvelope(store, "@ocas/output/put", hash)); } async function cmdGet(args: string[]): Promise { @@ -256,14 +273,14 @@ async function cmdGet(args: string[]): Promise { const store = await openStore(); const node = store.get(hash); if (node === null) die(`Node not found: ${hash}`); - out(await wrapEnvelope(store, "@ocas/output/get", node)); + await out(await wrapEnvelope(store, "@ocas/output/get", node)); } async function cmdHas(args: string[]): Promise { const hash = args[0]; if (!hash) die("Usage: ocas has "); const store = await openStore(); - out(await wrapEnvelope(store, "@ocas/output/has", store.has(hash))); + await out(await wrapEnvelope(store, "@ocas/output/has", store.has(hash))); } async function cmdVerify(args: string[]): Promise { @@ -279,7 +296,7 @@ async function cmdVerify(args: string[]): Promise { } else { status = validate(store, node) ? "ok" : "invalid"; } - out(await wrapEnvelope(store, "@ocas/output/verify", status)); + await out(await wrapEnvelope(store, "@ocas/output/verify", status)); } async function cmdRefs(args: string[]): Promise { @@ -289,7 +306,7 @@ async function cmdRefs(args: string[]): Promise { const node = store.get(hash); if (node === null) die(`Node not found: ${hash}`); const refHashes = refs(store, node); - out(await wrapEnvelope(store, "@ocas/output/refs", refHashes)); + await out(await wrapEnvelope(store, "@ocas/output/refs", refHashes)); } async function cmdWalk(args: string[]): Promise { @@ -325,13 +342,13 @@ async function cmdWalk(args: string[]): Promise { } printNode(hash, "", true); - out(await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n"))); + await out(await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n"))); } else { const hashes: Hash[] = []; walk(store, hash, (h) => { hashes.push(h); }); - out(await wrapEnvelope(store, "@ocas/output/walk", hashes)); + await out(await wrapEnvelope(store, "@ocas/output/walk", hashes)); } } @@ -349,7 +366,7 @@ async function cmdHash(args: string[]): Promise { const payload = isPipe ? await readStdinJson() : readJsonFile(file as string); const hash = await computeHash(typeHash, payload); const store = await openStore(); - out(await wrapEnvelope(store, "@ocas/output/hash", hash)); + await out(await wrapEnvelope(store, "@ocas/output/hash", hash)); } async function cmdRender(args: string[]): Promise { @@ -503,7 +520,7 @@ async function cmdVarSet(args: string[]): Promise { : undefined; const variable = varStore.set(name, value, options); - out(await wrapEnvelope(store, "@ocas/output/var-set", variable)); + await out(await wrapEnvelope(store, "@ocas/output/var-set", variable)); } catch (e) { if ( e instanceof InvalidVariableNameError || @@ -534,7 +551,7 @@ async function cmdVarGet(args: string[]): Promise { if (variable === null) { die(`Error: Variable not found: name=${name}, schema=${schema}`); } - out(await wrapEnvelope(store, "@ocas/output/var-get", variable)); + await out(await wrapEnvelope(store, "@ocas/output/var-get", variable)); } finally { varStore.close(); } @@ -559,11 +576,11 @@ async function cmdVarDelete(args: string[]): Promise { if (schema !== undefined) { // Precise deletion: remove specific (name, schema) variant const variable = varStore.remove(name, schema); - out(await wrapEnvelope(store, "@ocas/output/var-delete", variable)); + await out(await wrapEnvelope(store, "@ocas/output/var-delete", variable)); } else { // Batch deletion: remove all variants for this name const variables = varStore.remove(name); - out(await wrapEnvelope(store, "@ocas/output/var-delete", variables)); + await out(await wrapEnvelope(store, "@ocas/output/var-delete", variables)); } } catch (e) { if (e instanceof VariableNotFoundError) { @@ -600,7 +617,7 @@ async function cmdVarTag(args: string[]): Promise { delete: deleteNames.length > 0 ? deleteNames : undefined, }); - out(await wrapEnvelope(store, "@ocas/output/var-tag", variable)); + await out(await wrapEnvelope(store, "@ocas/output/var-tag", variable)); } catch (e) { if ( e instanceof VariableNotFoundError || @@ -643,7 +660,7 @@ async function cmdVarList(args: string[]): Promise { tags: Object.keys(tags).length > 0 ? tags : undefined, labels: labels.length > 0 ? labels : undefined, }); - out(await wrapEnvelope(store, "@ocas/output/var-list", variables)); + await out(await wrapEnvelope(store, "@ocas/output/var-list", variables)); } catch (e) { if (e instanceof InvalidVariableNameError) { die(`Error: ${e.message}`); @@ -708,7 +725,7 @@ async function cmdTemplateSet(args: string[]): Promise { const varName = `@ocas/template/text/${schemaHash}`; varStore.set(varName, contentHash); - out( + await out( await wrapEnvelope(store, "@ocas/output/template-set", { schemaHash, contentHash, @@ -749,7 +766,7 @@ async function cmdTemplateGet(args: string[]): Promise { die(`Error: Content not found in CAS: ${variable.value}`); } - out( + await out( await wrapEnvelope( store, "@ocas/output/template-get", @@ -777,7 +794,7 @@ async function cmdTemplateList(_args: string[]): Promise { contentHash: v.value, })); - out(await wrapEnvelope(store, "@ocas/output/template-list", templates)); + await out(await wrapEnvelope(store, "@ocas/output/template-list", templates)); } finally { varStore.close(); } @@ -798,7 +815,7 @@ async function cmdTemplateDelete(args: string[]): Promise { const stringHash = await resolveTypeHash("@ocas/string"); varStore.remove(varName, stringHash); - out( + await out( await wrapEnvelope(store, "@ocas/output/template-delete", { deleted: true, }), @@ -819,7 +836,7 @@ async function cmdGc(_args: string[]): Promise { try { const stats = gc(store, varStore); - out(await wrapEnvelope(store, "@ocas/output/gc", stats)); + await out(await wrapEnvelope(store, "@ocas/output/gc", stats)); } finally { varStore.close(); } @@ -832,19 +849,19 @@ async function cmdList(_args: string[]): Promise { const typeHash = await resolveTypeHash(typeFlag); const store = await openStore(); const hashes = Array.from(store.listByType(typeHash)); - out(await wrapEnvelope(store, "@ocas/output/list", hashes)); + await out(await wrapEnvelope(store, "@ocas/output/list", hashes)); } async function cmdListMeta(_args: string[]): Promise { const store = await openStore(); const hashes = store.listMeta(); - out(await wrapEnvelope(store, "@ocas/output/list-meta", hashes)); + await out(await wrapEnvelope(store, "@ocas/output/list-meta", hashes)); } async function cmdListSchema(_args: string[]): Promise { const store = await openStore(); const hashes = store.listSchemas(); - out(await wrapEnvelope(store, "@ocas/output/list-schema", hashes)); + await out(await wrapEnvelope(store, "@ocas/output/list-schema", hashes)); } function printUsage(): void { @@ -883,6 +900,7 @@ Flags: --home Store directory (default: $OCAS_HOME or ~/.ocas) --var-db Variable database path (default: /variables.db) --json Compact JSON output + --render, -r Render output inline (equivalent to | ocas render -p) --schema Schema hash filter for var get/delete/tag/list --tag Tag/label (can be repeated): key:value (tag), name (label), :name (delete) --inline Inline text content for template set diff --git a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap index ec785a3..920d1fc 100644 --- a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap +++ b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap @@ -41,6 +41,7 @@ Flags: --home Store directory (default: $OCAS_HOME or ~/.ocas) --var-db Variable database path (default: /variables.db) --json Compact JSON output + --render, -r Render output inline (equivalent to | ocas render -p) --schema Schema hash filter for var get/delete/tag/list --tag Tag/label (can be repeated): key:value (tag), name (label), :name (delete) --inline Inline text content for template set -- 2.43.0