diff --git a/packages/cli-json-cas/src/__snapshots__/e2e.test.ts.snap b/packages/cli-json-cas/src/__snapshots__/e2e.test.ts.snap index 9d9f668..7ef613a 100644 --- a/packages/cli-json-cas/src/__snapshots__/e2e.test.ts.snap +++ b/packages/cli-json-cas/src/__snapshots__/e2e.test.ts.snap @@ -2,15 +2,23 @@ exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = ` { - "payload": { - "age": 30, - "name": "Alice", + "type": "ASE7K6A0HG8W9", + "value": { + "payload": { + "age": 30, + "name": "Alice", + }, + "type": "7XX5H51CVD9H0", }, - "type": "7XX5H51CVD9H0", } `; -exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `"ok"`; +exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = ` +{ + "type": "8E2M8H30BHXS8", + "value": "ok", +} +`; exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `""`; @@ -216,16 +224,16 @@ exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = ` "Usage: json-cas [--store ] [--json] [args] Commands: - put Store node, print hash - get Print node as JSON - has Print true/false - verify Verify integrity + schema, print ok/corrupted/invalid + put Store node, print { type, value } envelope (value=hash) + get Print node as { type, value } envelope + has Print { type, value } envelope (value=boolean) + verify Verify integrity + schema → { type, value } (value=ok/corrupted/invalid) refs List direct cas_ref edges walk [--format tree] Recursive traversal - hash Compute hash without storing (dry run) + hash Compute hash without storing → { type, value } envelope render [options] Render node as YAML with resolution decay render --pipe/-p [options] Render { type, value } from stdin - list --type List all hashes for a given type + list --type List hashes for a type → { type, value } envelope (value=string[]) var set [--tag ...] Create/update a variable var get --schema Get a variable by name + schema var delete [--schema ] Delete variable(s) diff --git a/packages/cli-json-cas/src/cli.test.ts b/packages/cli-json-cas/src/cli.test.ts index 57cd44c..7537b9d 100644 --- a/packages/cli-json-cas/src/cli.test.ts +++ b/packages/cli-json-cas/src/cli.test.ts @@ -15,6 +15,11 @@ import { createFsStore, openStore as openFsStore } from "@uncaged/json-cas-fs"; const pkgPath = resolve(import.meta.dir, "../package.json"); const entrypoint = resolve(import.meta.dir, "index.ts"); +/** Extract the `value` field from a { type, value } envelope JSON string. */ +function envValue(json: string): unknown { + return (JSON.parse(json.trim()) as { value: unknown }).value; +} + /** * Register a schema directly via the library (CLI schema put was removed). * Returns the type hash. @@ -168,8 +173,8 @@ describe("@ Alias Resolution - put", () => { expect(exitCode).toBe(0); expect(stderr).toBe(""); - // Should output a valid hash (13 chars) - expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + // Should output an envelope whose value is a valid hash (13 chars) + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); test("ucas put @number should resolve alias", async () => { @@ -185,7 +190,7 @@ describe("@ Alias Resolution - put", () => { ); expect(exitCode).toBe(0); - expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); test("ucas put @object should resolve alias", async () => { @@ -201,7 +206,7 @@ describe("@ Alias Resolution - put", () => { ); expect(exitCode).toBe(0); - expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); test("ucas put @invalid should fail", async () => { @@ -236,7 +241,7 @@ describe("@ Alias Resolution - hash", () => { expect(exitCode).toBe(0); expect(stderr).toBe(""); - expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); }); @@ -313,10 +318,10 @@ describe("Issue #50: Schema Validation in put", () => { expect(exitCode).toBe(0); expect(stderr).toBe(""); - expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); // Verify node was stored - const hash = stdout.trim(); + const hash = envValue(stdout) as string; const { exitCode: hasExitCode } = await runCli(["has", hash], tmpStore); expect(hasExitCode).toBe(0); } finally { @@ -355,7 +360,7 @@ describe("Issue #50: Schema Validation in put", () => { ); expect(exitCode).toBe(0); - expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } finally { rmSync(tmpStore, { recursive: true, force: true }); } @@ -403,7 +408,7 @@ describe("Issue #50: Schema Validation in put", () => { ); expect(exitCode).toBe(0); - expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } finally { rmSync(tmpStore, { recursive: true, force: true }); } @@ -423,7 +428,7 @@ describe("Issue #50: Schema Validation in put", () => { ); expect(exitCode).toBe(0); - expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } finally { rmSync(tmpStore, { recursive: true, force: true }); } @@ -469,7 +474,7 @@ describe("Issue #50: Schema Validation in put", () => { ["has", "0000000000000"], tmpStore, ); - expect(hasOutput.trim()).toBe("false"); + expect(envValue(hasOutput)).toBe(false); } finally { rmSync(tmpStore, { recursive: true, force: true }); } @@ -692,7 +697,7 @@ describe("Issue #50: Schema Validation in put", () => { ); expect(exitCode).toBe(0); - expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); } finally { rmSync(tmpStore, { recursive: true, force: true }); } @@ -850,33 +855,30 @@ describe("Suite 6: CLI Integration with Templates", () => { // Create node const nodeFile = join(tmpStore, "node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Alice" })); - const { stdout: nodeHash } = await runCli( + const { stdout: nodeOut } = await runCli( ["put", schemaHash.trim(), nodeFile], tmpStore, ); + const nodeHash = envValue(nodeOut) as string; // Create template file (JSON-encoded string) const templateFile = join(tmpStore, "template.json"); writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!")); - const { stdout: tmplHash } = await runCli( + const { stdout: tmplOut } = await runCli( ["put", "@string", templateFile], tmpStore, ); + const tmplHash = envValue(tmplOut) as string; // Register template await runCli( - [ - "var", - "set", - `@ucas/template/text/${schemaHash.trim()}`, - tmplHash.trim(), - ], + ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash], tmpStore, ); // Render with template const { stdout: output, exitCode } = await runCli( - ["render", nodeHash.trim()], + ["render", nodeHash], tmpStore, ); @@ -911,21 +913,23 @@ describe("Suite 6: CLI Integration with Templates", () => { // Create child node const childFile = join(tmpStore, "child.json"); writeFileSync(childFile, JSON.stringify({ value: "child", child: null })); - const { stdout: childHash } = await runCli( + const { stdout: childOut } = await runCli( ["put", schemaHash.trim(), childFile], tmpStore, ); + const childHash = envValue(childOut) as string; // Create parent node const parentFile = join(tmpStore, "parent.json"); writeFileSync( parentFile, - JSON.stringify({ value: "parent", child: childHash.trim() }), + JSON.stringify({ value: "parent", child: childHash }), ); - const { stdout: parentHash } = await runCli( + const { stdout: parentOut } = await runCli( ["put", schemaHash.trim(), parentFile], tmpStore, ); + const parentHash = envValue(parentOut) as string; // Create template showing resolution (JSON-encoded string) const templateFile = join(tmpStore, "template.json"); @@ -933,25 +937,21 @@ describe("Suite 6: CLI Integration with Templates", () => { templateFile, JSON.stringify("{{ payload.value }}(res={{ resolution }})"), ); - const { stdout: tmplHash } = await runCli( + const { stdout: tmplOut } = await runCli( ["put", "@string", templateFile], tmpStore, ); + const tmplHash = envValue(tmplOut) as string; // Register template await runCli( - [ - "var", - "set", - `@ucas/template/text/${schemaHash.trim()}`, - tmplHash.trim(), - ], + ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash], tmpStore, ); // Render with custom decay const { stdout: output, exitCode } = await runCli( - ["render", parentHash.trim(), "--decay", "0.7"], + ["render", parentHash, "--decay", "0.7"], tmpStore, ); @@ -979,10 +979,11 @@ describe("Suite 6: CLI Integration with Templates", () => { const nodeFile = join(tmpStore, "node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Bob" })); - const { stdout: nodeHash } = await runCli( + const { stdout: nodeOut } = await runCli( ["put", schemaHash.trim(), nodeFile], tmpStore, ); + const nodeHash = envValue(nodeOut) as string; // Create template (JSON-encoded string) const templateFile = join(tmpStore, "template.json"); @@ -990,25 +991,21 @@ describe("Suite 6: CLI Integration with Templates", () => { templateFile, JSON.stringify("Greetings {{ payload.name }}!"), ); - const { stdout: tmplHash } = await runCli( + const { stdout: tmplOut } = await runCli( ["put", "@string", templateFile], tmpStore, ); + const tmplHash = envValue(tmplOut) as string; await runCli( - [ - "var", - "set", - `@ucas/template/text/${schemaHash.trim()}`, - tmplHash.trim(), - ], + ["var", "set", `@ucas/template/text/${schemaHash.trim()}`, tmplHash], tmpStore, ); const { stdout: output, exitCode } = await runCli( [ "render", - nodeHash.trim(), + nodeHash, "--resolution", "0.8", "--decay", @@ -1043,14 +1040,15 @@ describe("Suite 6: CLI Integration with Templates", () => { const nodeFile = join(tmpStore, "node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" })); - const { stdout: nodeHash } = await runCli( + const { stdout: nodeOut } = await runCli( ["put", schemaHash.trim(), nodeFile], tmpStore, ); + const nodeHash = envValue(nodeOut) as string; // No template registered - should fall back to YAML const { stdout: output, exitCode } = await runCli( - ["render", nodeHash.trim()], + ["render", nodeHash], tmpStore, ); @@ -1079,13 +1077,14 @@ describe("Suite 6: CLI Integration with Templates", () => { const nodeFile = join(tmpStore, "node.json"); writeFileSync(nodeFile, JSON.stringify({ name: "Test" })); - const { stdout: nodeHash } = await runCli( + const { stdout: nodeOut } = await runCli( ["put", schemaHash.trim(), nodeFile], tmpStore, ); + const nodeHash = envValue(nodeOut) as string; const { exitCode, stderr } = await runCli( - ["render", nodeHash.trim(), "--decay", "1.5"], + ["render", nodeHash, "--decay", "1.5"], tmpStore, ); @@ -1126,14 +1125,15 @@ describe("Suite 6: CLI Integration with Templates", () => { // Create and store a simple string node const nodeFile = join(tmpStore, "test.json"); writeFileSync(nodeFile, JSON.stringify("hello world")); - const { stdout: nodeHash } = await runCli( + const { stdout: nodeOut } = await runCli( ["put", stringType, nodeFile], tmpStore, ); + const nodeHash = envValue(nodeOut) as string; // Render the valid hash const { exitCode, stdout, stderr } = await runCli( - ["render", nodeHash.trim()], + ["render", nodeHash], tmpStore, ); diff --git a/packages/cli-json-cas/src/e2e.test.ts b/packages/cli-json-cas/src/e2e.test.ts index 140ea88..1858e6b 100644 --- a/packages/cli-json-cas/src/e2e.test.ts +++ b/packages/cli-json-cas/src/e2e.test.ts @@ -54,6 +54,11 @@ function stripVolatile(json: string): unknown { return strip(JSON.parse(json)); } +/** Extract the `value` field from a { type, value } envelope JSON string. */ +function envValue(json: string): unknown { + return (JSON.parse(json) as { value: unknown }).value; +} + // ---- Phase 1: CAS Core ---- describe("Phase 1: CAS Core", () => { @@ -88,8 +93,8 @@ describe("Phase 1: CAS Core", () => { writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 })); const { stdout, exitCode } = await runCli(["put", typeHash, nodeFile]); expect(exitCode).toBe(0); - expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); - nodeHash = stdout; + expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); + nodeHash = envValue(stdout) as string; }); test("1.6 get returns node JSON (snapshot)", async () => { @@ -101,19 +106,19 @@ describe("Phase 1: CAS Core", () => { test("1.7 has returns true for existing node", async () => { const { stdout, exitCode } = await runCli(["has", nodeHash]); expect(exitCode).toBe(0); - expect(stdout).toBe("true"); + expect(envValue(stdout)).toBe(true); }); test("1.8 has returns false for non-existing hash", async () => { const { stdout, exitCode } = await runCli(["has", "AAAAAAAAAAAAA"]); expect(exitCode).toBe(0); - expect(stdout).toBe("false"); + expect(envValue(stdout)).toBe(false); }); test("1.9 verify returns ok for valid node", async () => { const { stdout, exitCode } = await runCli(["verify", nodeHash]); expect(exitCode).toBe(0); - expect(stdout).toMatchSnapshot(); + expect(stripVolatile(stdout)).toMatchSnapshot(); }); test("1.10 refs lists direct references (snapshot)", async () => { @@ -132,13 +137,13 @@ describe("Phase 1: CAS Core", () => { const nodeFile = join(tmpStore, "test-node.json"); const { stdout, exitCode } = await runCli(["hash", typeHash, nodeFile]); expect(exitCode).toBe(0); - expect(stdout).toBe(nodeHash); + expect(envValue(stdout)).toBe(nodeHash); }); test("1.13 list --type returns nodes of that type", async () => { const { stdout, exitCode } = await runCli(["list", "--type", typeHash]); expect(exitCode).toBe(0); - expect(stdout).toContain(nodeHash); + expect(envValue(stdout)).toContain(nodeHash); }); }); @@ -163,7 +168,7 @@ describe("Phase 2: Schema Validation", () => { test("2.2 verify on valid node returns ok (hash + schema)", async () => { const { stdout, exitCode } = await runCli(["verify", nodeHash]); expect(exitCode).toBe(0); - expect(stdout).toBe("ok"); + expect(envValue(stdout)).toBe("ok"); }); test("2.3 put against non-existent schema hash fails", async () => { @@ -222,12 +227,13 @@ describe("Phase 3: Variable System", () => { test("3.5 var set upsert updates existing variable", async () => { const node2File = join(tmpStore, "node2.json"); writeFileSync(node2File, JSON.stringify({ name: "Bob", age: 25 })); - const { stdout: node2Hash } = await runCli(["put", typeHash, node2File]); + const { stdout: node2Out } = await runCli(["put", typeHash, node2File]); + const node2Hash = envValue(node2Out) as string; const { exitCode, stdout } = await runCli([ "var", "set", "myapp/config", - node2Hash.trim(), + node2Hash, ]); expect(exitCode).toBe(0); expect(stripVolatile(stdout)).toMatchSnapshot(); @@ -405,7 +411,7 @@ describe("Phase 6: GC", () => { const gcNodeFile = join(tmpStore, "gc-node.json"); writeFileSync(gcNodeFile, JSON.stringify({ name: "GcAlice", age: 30 })); const { stdout } = await runCli(["put", typeHash, gcNodeFile]); - gcNodeHash = stdout.trim(); + gcNodeHash = envValue(stdout) as string; // Set a var referencing this node so it survives GC during Phase 6 await runCli(["var", "set", "gc-test/ref", gcNodeHash]); }); @@ -428,25 +434,21 @@ describe("Phase 6: GC", () => { const { exitCode } = await runCli(["gc"]); expect(exitCode).toBe(0); const { stdout } = await runCli(["has", gcNodeHash]); - expect(stdout).toBe("true"); + expect(envValue(stdout)).toBe(true); }); test("6.3 gc reclaims orphan node", async () => { const orphanFile = join(tmpStore, "orphan.json"); writeFileSync(orphanFile, JSON.stringify({ name: "Orphan", age: 99 })); - const { stdout: orphanHashRaw } = await runCli([ - "put", - typeHash, - orphanFile, - ]); - const orphanHash = orphanHashRaw.trim(); + const { stdout: orphanOut } = await runCli(["put", typeHash, orphanFile]); + const orphanHash = envValue(orphanOut) as string; const { stdout: beforeGc } = await runCli(["has", orphanHash]); - expect(beforeGc).toBe("true"); + expect(envValue(beforeGc)).toBe(true); await runCli(["gc"]); const { stdout: afterGc } = await runCli(["has", orphanHash]); - expect(afterGc).toBe("false"); + expect(envValue(afterGc)).toBe(false); }); }); diff --git a/packages/cli-json-cas/src/index.ts b/packages/cli-json-cas/src/index.ts index c9536bf..09de50d 100644 --- a/packages/cli-json-cas/src/index.ts +++ b/packages/cli-json-cas/src/index.ts @@ -22,6 +22,7 @@ import { validate, verify, walk, + wrapEnvelope, } from "@uncaged/json-cas"; import { openStore as openFsStore } from "@uncaged/json-cas-fs"; @@ -253,7 +254,7 @@ async function cmdPut(args: string[]): Promise { } const hash = await store.put(typeHash, payload); - console.log(hash); + out(await wrapEnvelope(store, "@output/put", hash)); } async function cmdGet(args: string[]): Promise { @@ -262,14 +263,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(node); + out(await wrapEnvelope(store, "@output/get", node)); } async function cmdHas(args: string[]): Promise { const hash = args[0]; if (!hash) die("Usage: json-cas has "); const store = await openStore(); - console.log(String(store.has(hash))); + out(await wrapEnvelope(store, "@output/has", store.has(hash))); } async function cmdVerify(args: string[]): Promise { @@ -279,12 +280,13 @@ async function cmdVerify(args: string[]): Promise { const node = store.get(hash); if (node === null) die(`Node not found: ${hash}`); const ok = await verify(hash, node); + let status: string; if (!ok) { - console.log("corrupted"); + status = "corrupted"; } else { - const valid = validate(store, node); - console.log(valid ? "ok" : "invalid"); + status = validate(store, node) ? "ok" : "invalid"; } + out(await wrapEnvelope(store, "@output/verify", status)); } async function cmdRefs(args: string[]): Promise { @@ -346,7 +348,8 @@ async function cmdHash(args: string[]): Promise { const typeHash = await resolveTypeHash(typeHashOrAlias); const payload = readJsonFile(file); const hash = await computeHash(typeHash, payload); - console.log(hash); + const store = await openStore(); + out(await wrapEnvelope(store, "@output/hash", hash)); } async function cmdRender(args: string[]): Promise { @@ -826,9 +829,8 @@ async function cmdList(_args: string[]): Promise { die("Usage: json-cas list --type "); const typeHash = await resolveTypeHash(typeFlag); const store = await openStore(); - for (const hash of store.listByType(typeHash)) { - console.log(hash); - } + const hashes = Array.from(store.listByType(typeHash)); + out(await wrapEnvelope(store, "@output/list", hashes)); } function printUsage(): void { @@ -836,16 +838,16 @@ function printUsage(): void { Usage: json-cas [--store ] [--json] [args] Commands: - put Store node, print hash - get Print node as JSON - has Print true/false - verify Verify integrity + schema, print ok/corrupted/invalid + put Store node, print { type, value } envelope (value=hash) + get Print node as { type, value } envelope + has Print { type, value } envelope (value=boolean) + verify Verify integrity + schema → { type, value } (value=ok/corrupted/invalid) refs List direct cas_ref edges walk [--format tree] Recursive traversal - hash Compute hash without storing (dry run) + hash Compute hash without storing → { type, value } envelope render [options] Render node as YAML with resolution decay render --pipe/-p [options] Render { type, value } from stdin - list --type List all hashes for a given type + list --type List hashes for a type → { type, value } envelope (value=string[]) var set [--tag ...] Create/update a variable var get --schema Get a variable by name + schema var delete [--schema ] Delete variable(s)