feat: remove cat/schema commands, add list --type, enhance verify (Phase 1b) #77

Merged
xiaoju merged 1 commits from fix/74-remove-cat-schema into main 2026-05-31 14:52:37 +00:00
4 changed files with 90 additions and 493 deletions
@@ -1,32 +1,5 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`Phase 1: CAS Core 1.3 schema get returns schema JSON (snapshot) 1`] = `
"{
"type": "object",
"required": [
"name"
],
"properties": {
"age": {
"type": "number"
},
"name": {
"type": "string"
}
},
"additionalProperties": false
}"
`;
exports[`Phase 1: CAS Core 1.4 schema list shows registered schemas 1`] = `
"8J5E9WCD54ZJZ (unnamed)
89ZQWMV9PTC7B (unnamed)
86QB0Q5VA7797 (unnamed)
AKEX4KYV98MGT (unnamed)
AGSJVKM01WNKZ (unnamed)
7XX5H51CVD9H0 (unnamed)"
`;
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
{
"payload": {
@@ -43,25 +16,6 @@ exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `"
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `"ERARPP19YJT05"`;
exports[`Phase 1: CAS Core 1.13 cat returns full node (snapshot) 1`] = `
{
"payload": {
"age": 30,
"name": "Alice",
},
"type": "7XX5H51CVD9H0",
}
`;
exports[`Phase 1: CAS Core 1.14 cat --payload returns only payload (snapshot) 1`] = `
"{
"age": 30,
"name": "Alice"
}"
`;
exports[`Phase 2: Schema Validation 2.2 schema validate on valid node returns valid 1`] = `"valid"`;
exports[`Phase 2: Schema Validation 2.3 put against non-existent schema hash fails 1`] = `"Schema not found: AAAAAAAAAAAAA"`;
exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
@@ -258,26 +212,20 @@ exports[`Phase 7: Edge Cases 7.3 var set empty name errors 1`] = `"Usage: json-c
exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Segment "invalid name!" contains invalid characters (only @, a-z, A-Z, 0-9, ., _, - allowed)"`;
exports[`Phase 7: Edge Cases 7.5 schema put invalid schema errors 1`] = `"Invalid schema: input does not conform to the json-cas JSON Schema meta-schema"`;
exports[`Phase 7: Edge Cases 7.6 no subcommand shows help text 1`] = `
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
"Usage: json-cas [--store <path>] [--json] <command> [args]
Commands:
schema put <file.json> Register schema, print type hash
schema get <type-hash> Print schema JSON
schema list List all schemas (name + hash)
schema validate <hash> Validate node against its schema
put <type-hash> <file.json> Store node, print hash
get <hash> Print node as JSON
has <hash> Print true/false
verify <hash> Verify integrity, print ok/corrupted
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin
cat <hash> [--payload] Output node (--payload for payload only)
list --type <hash-or-alias> List all hashes for a given type
var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s)
+45 -290
View File
@@ -1,13 +1,36 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { bootstrap } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { JSONSchema } from "@uncaged/json-cas";
import { bootstrap, putSchema } from "@uncaged/json-cas";
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");
/**
* Register a schema directly via the library (CLI schema put was removed).
* Returns the type hash.
*/
async function putSchemaFile(
storePath: string,
schemaFilePath: string,
): Promise<string> {
const store = await openFsStore(storePath);
const schema = JSON.parse(
readFileSync(schemaFilePath, "utf-8"),
) as JSONSchema;
const hash = await putSchema(store, schema);
return hash;
}
async function runCli(
args: string[],
storePath?: string,
@@ -130,86 +153,6 @@ async function runCliAlias(...args: string[]): Promise<{
};
}
describe("@ Alias Resolution - schema get", () => {
test("ucas schema get @string should work", async () => {
await runCliAlias("init"); // Initialize store
const { stdout, stderr, exitCode } = await runCliAlias(
"schema",
"get",
"@string",
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "string" });
});
test("ucas schema get @number should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@number");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "number" });
});
test("ucas schema get @object should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@object");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "object" });
});
test("ucas schema get @array should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@array");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "array" });
});
test("ucas schema get @bool should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@bool");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toEqual({ type: "boolean" });
});
test("ucas schema get @schema should work", async () => {
await runCliAlias("init");
const { stdout, exitCode } = await runCliAlias("schema", "get", "@schema");
expect(exitCode).toBe(0);
const schema = JSON.parse(stdout);
expect(schema).toHaveProperty("type", "object");
expect(schema).toHaveProperty(
"description",
"json-cas JSON Schema meta-schema",
);
});
test("ucas schema get @invalid should fail gracefully", async () => {
await runCliAlias("init");
const { stderr, exitCode } = await runCliAlias("schema", "get", "@invalid");
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Schema not found");
});
});
describe("@ Alias Resolution - put", () => {
test("ucas put @string <file> should resolve alias", async () => {
await runCliAlias("init");
@@ -357,10 +300,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create valid payload
const payloadFile = join(tmpStore, "payload.json");
@@ -403,10 +343,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with only required properties
const payloadFile = join(tmpStore, "payload.json");
@@ -448,10 +385,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with nested structure
const payloadFile = join(tmpStore, "payload.json");
@@ -513,10 +447,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with name as number
const payloadFile = join(tmpStore, "payload.json");
@@ -560,10 +491,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Empty payload
const payloadFile = join(tmpStore, "payload.json");
@@ -596,10 +524,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with extra property
const payloadFile = join(tmpStore, "payload.json");
@@ -634,10 +559,7 @@ describe("Issue #50: Schema Validation in put", () => {
items: { type: "string" },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload is an object
const payloadFile = join(tmpStore, "payload.json");
@@ -676,10 +598,7 @@ describe("Issue #50: Schema Validation in put", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Payload with wrong nested type
const payloadFile = join(tmpStore, "payload.json");
@@ -760,10 +679,7 @@ describe("Issue #50: Schema Validation in put", () => {
additionalProperties: false,
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Invalid payload
const payloadFile = join(tmpStore, "payload.json");
@@ -798,10 +714,7 @@ describe("Issue #50: Schema Validation in put", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Valid cas_ref
const validFile = join(tmpStore, "valid.json");
@@ -843,11 +756,8 @@ describe("Issue #50: Schema Validation in put", () => {
}),
);
const { exitCode } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).toBe(0);
const hash = await putSchemaFile(tmpStore, schemaFile);
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
@@ -869,10 +779,7 @@ describe("Issue #50: Schema Validation in put", () => {
required: ["name"],
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const payloadFile = join(tmpStore, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ name: 123 }));
@@ -903,10 +810,7 @@ describe("Issue #50: Schema Validation in put", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const payloadFile = join(tmpStore, "payload.json");
writeFileSync(payloadFile, JSON.stringify({ name: 123 }));
@@ -941,10 +845,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create node
const nodeFile = join(tmpStore, "node.json");
@@ -1005,10 +906,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
},
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
// Create child node
const childFile = join(tmpStore, "child.json");
@@ -1077,10 +975,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
@@ -1144,10 +1039,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
@@ -1183,10 +1075,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
properties: { name: { type: "string" } },
}),
);
const { stdout: schemaHash } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
const schemaHash = await putSchemaFile(tmpStore, schemaFile);
const nodeFile = join(tmpStore, "node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
@@ -1309,140 +1198,6 @@ describe("Suite 6: CLI Integration with Templates", () => {
});
});
// ---- schema put - invalid schema error handling ----
describe("schema put - invalid schema error handling", () => {
test("invalid schema - unknown type value shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(schemaFile, JSON.stringify({ type: "invalid" }));
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - unknown key shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({ type: "string", unknownKey: true }),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - invalid nested schema shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: {
name: { type: "invalid" },
},
}),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("invalid schema - non-object root shows clean error", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "invalid-schema.json");
writeFileSync(schemaFile, JSON.stringify(["type", "string"]));
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("Invalid schema");
expect(stderr).not.toContain("at ");
expect(stdout).toBe("");
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
test("valid schema still works (regression)", async () => {
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
try {
await runCli(["init"], tmpStore);
const schemaFile = join(tmpStore, "valid-schema.json");
writeFileSync(
schemaFile,
JSON.stringify({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name"],
}),
);
const { exitCode, stderr, stdout } = await runCli(
["schema", "put", schemaFile],
tmpStore,
);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
} finally {
rmSync(tmpStore, { recursive: true, force: true });
}
});
});
// Store validation tests removed - with auto-bootstrap in Phase 1a,
// stores are automatically created and bootstrapped when opened.
// Issue #55 validation is no longer applicable.
+20 -49
View File
@@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -57,7 +57,7 @@ function stripVolatile(json: string): unknown {
// ---- Phase 1: CAS Core ----
describe("Phase 1: CAS Core", () => {
test("1.1 schema put returns type hash (auto-bootstraps store)", async () => {
test("1.1 init + put with @object bootstraps store", async () => {
const schemaFile = join(tmpStore, "test-schema.json");
writeFileSync(
schemaFile,
@@ -71,23 +71,16 @@ describe("Phase 1: CAS Core", () => {
additionalProperties: false,
}),
);
const { stdout, exitCode } = await runCli(["schema", "put", schemaFile]);
expect(exitCode).toBe(0);
expect(stdout).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
typeHash = stdout;
});
test("1.3 schema get returns schema JSON (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["schema", "get", typeHash]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
});
test("1.4 schema list shows registered schemas", async () => {
const { stdout, exitCode } = await runCli(["schema", "list"]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
expect(stdout).toContain(typeHash);
// Use putSchema via the library to register schema, since CLI schema put is removed
const { openStore: openFsStore } = await import("@uncaged/json-cas-fs");
const { putSchema } = await import("@uncaged/json-cas");
const store = await openFsStore(tmpStore);
const hash = await putSchema(
store,
JSON.parse(readFileSync(schemaFile, "utf-8")),
);
typeHash = hash;
expect(typeHash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("1.5 put returns node hash", async () => {
@@ -142,19 +135,10 @@ describe("Phase 1: CAS Core", () => {
expect(stdout).toBe(nodeHash);
});
test("1.13 cat returns full node (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["cat", 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(stripVolatile(stdout)).toMatchSnapshot();
});
test("1.14 cat --payload returns only payload (snapshot)", async () => {
const { stdout, exitCode } = await runCli(["cat", nodeHash, "--payload"]);
expect(exitCode).toBe(0);
expect(stdout).toMatchSnapshot();
const parsed = JSON.parse(stdout) as Record<string, unknown>;
expect(parsed).not.toHaveProperty("type");
expect(parsed).toHaveProperty("name", "Alice");
expect(stdout).toContain(nodeHash);
});
});
@@ -176,10 +160,10 @@ describe("Phase 2: Schema Validation", () => {
// Do NOT snapshot stderr — it embeds a machine-specific tmp path
});
test("2.2 schema validate on valid node returns valid", async () => {
const { stdout, exitCode } = await runCli(["schema", "validate", nodeHash]);
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).toMatchSnapshot();
expect(stdout).toBe("ok");
});
test("2.3 put against non-existent schema hash fails", async () => {
@@ -504,20 +488,7 @@ describe("Phase 7: Edge Cases", () => {
expect(stderr).toMatchSnapshot();
});
test("7.5 schema put invalid schema errors", async () => {
const badSchemaFile = join(tmpStore, "bad-schema.json");
writeFileSync(badSchemaFile, JSON.stringify({ type: "invalid" }));
const { stdout, stderr, exitCode } = await runCli([
"schema",
"put",
badSchemaFile,
]);
expect(exitCode).not.toBe(0);
expect(stdout).toBe("");
expect(stderr).toMatchSnapshot();
});
test("7.6 no subcommand shows help text", async () => {
test("7.5 no subcommand shows help text", async () => {
const { stdout, stderr, exitCode: _exitCode } = await runCli([]);
const combined = stdout + stderr;
expect(combined.length).toBeGreaterThan(0);
@@ -525,7 +496,7 @@ describe("Phase 7: Edge Cases", () => {
expect(combined.toLowerCase()).toContain("usage");
});
test("7.7 --store path is a file errors", async () => {
test("7.6 --store path is a file errors", async () => {
const fileAsStore = join(tmpStore, "not-a-directory");
writeFileSync(fileAsStore, "test");
const proc = Bun.spawn(
+22 -99
View File
@@ -17,7 +17,6 @@ import {
refs,
renderAsync,
renderDirect,
SchemaValidationError,
TagLabelConflictError,
VariableNotFoundError,
validate,
@@ -41,6 +40,7 @@ const VALUE_FLAGS = new Set([
"decay",
"epsilon",
"inline",
"type",
]);
function parseArgs(argv: string[]): { flags: Flags; positional: string[] } {
@@ -227,61 +227,6 @@ function parseTagsLabels(args: string[]): {
// ---- Commands ----
async function cmdSchemaPut(args: string[]): Promise<void> {
const file = args[0];
if (!file) die("Usage: json-cas schema put <file.json>");
const schema = readJsonFile(file) as JSONSchema;
const store = await openStore();
try {
const hash = await putSchema(store, schema);
console.log(hash);
} catch (e) {
if (e instanceof SchemaValidationError) {
die(e.message);
}
throw e;
}
}
async function cmdSchemaGet(args: string[]): Promise<void> {
const hashOrAlias = args[0];
if (!hashOrAlias) die("Usage: json-cas schema get <type-hash>");
const hash = await resolveTypeHash(hashOrAlias);
const store = await openStore();
const schema = getSchema(store, hash);
if (schema === null) die(`Schema not found: ${hashOrAlias}`);
out(schema);
}
async function cmdSchemaList(): Promise<void> {
const store = await openStore();
const builtinSchemas = await bootstrap(store);
const metaHash = builtinSchemas["@schema"];
if (!metaHash) throw new Error("Meta-schema not found");
for (const hash of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null) {
const schema = node.payload as JSONSchema;
const name =
(schema.title as string | undefined) ??
(schema.description as string | undefined) ??
"(unnamed)";
console.log(`${hash} ${name}`);
}
}
}
async function cmdSchemaValidate(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas schema validate <hash>");
const store = await openStore();
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
const valid = validate(store, node);
console.log(valid ? "valid" : "invalid");
}
async function cmdPut(args: string[]): Promise<void> {
const typeHashOrAlias = args[0];
const file = args[1];
@@ -334,7 +279,12 @@ async function cmdVerify(args: string[]): Promise<void> {
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
const ok = await verify(hash, node);
console.log(ok ? "ok" : "corrupted");
if (!ok) {
console.log("corrupted");
} else {
const valid = validate(store, node);
console.log(valid ? "ok" : "invalid");
}
}
async function cmdRefs(args: string[]): Promise<void> {
@@ -509,19 +459,6 @@ async function cmdRender(args: string[]): Promise<void> {
}
}
async function cmdCat(args: string[]): Promise<void> {
const hash = args[0];
if (!hash) die("Usage: json-cas cat <hash>");
const store = await openStore();
const node = store.get(hash);
if (node === null) die(`Node not found: ${hash}`);
if (flags.payload === true) {
out(node.payload);
} else {
out(node);
}
}
async function cmdVarSet(args: string[]): Promise<void> {
const name = args[0];
const value = args[1];
@@ -883,25 +820,32 @@ async function cmdGc(_args: string[]): Promise<void> {
}
}
async function cmdList(_args: string[]): Promise<void> {
const typeFlag = flags.type;
if (typeof typeFlag !== "string")
die("Usage: json-cas list --type <hash-or-alias>");
const typeHash = await resolveTypeHash(typeFlag);
const store = await openStore();
for (const hash of store.listByType(typeHash)) {
console.log(hash);
}
}
function printUsage(): void {
console.log(`\
Usage: json-cas [--store <path>] [--json] <command> [args]
Commands:
schema put <file.json> Register schema, print type hash
schema get <type-hash> Print schema JSON
schema list List all schemas (name + hash)
schema validate <hash> Validate node against its schema
put <type-hash> <file.json> Store node, print hash
get <hash> Print node as JSON
has <hash> Print true/false
verify <hash> Verify integrity, print ok/corrupted
verify <hash> Verify integrity + schema, print ok/corrupted/invalid
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
render <hash> [options] Render node as YAML with resolution decay
render --pipe/-p [options] Render { type, value } from stdin
cat <hash> [--payload] Output node (--payload for payload only)
list --type <hash-or-alias> List all hashes for a given type
var set <name> <hash> [--tag <tag>...] Create/update a variable
var get <name> --schema <hash> Get a variable by name + schema
var delete <name> [--schema <hash>] Delete variable(s)
@@ -936,27 +880,6 @@ if (!cmd) {
}
switch (cmd) {
case "schema": {
const [sub, ...subRest] = rest;
switch (sub) {
case "put":
await cmdSchemaPut(subRest);
break;
case "get":
await cmdSchemaGet(subRest);
break;
case "list":
await cmdSchemaList();
break;
case "validate":
await cmdSchemaValidate(subRest);
break;
default:
die(`Unknown schema subcommand: ${sub ?? "(none)"}`);
}
break;
}
case "put":
await cmdPut(rest);
break;
@@ -989,8 +912,8 @@ switch (cmd) {
await cmdRender(rest);
break;
case "cat":
await cmdCat(rest);
case "list":
await cmdList(rest);
break;
case "var": {