Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdb77fd1e3 | |||
| b7fc03fa16 | |||
| 9367a4141a | |||
| bfec74fe8e | |||
| 70af05adf5 | |||
| ceba0b5d43 | |||
| a050160bee | |||
| 515f4d32f9 | |||
| c03180e66c |
@@ -1,5 +1,33 @@
|
||||
# @uncaged/cli-json-cas
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Unified `{ type, value }` envelope output for all CLI commands (RFC [#67](https://github.com/uncaged/json-cas/issues/67)).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `init`, `bootstrap`, `cat`, `schema put/get/list/validate` commands
|
||||
- All CLI command outputs now wrapped in `{ type, value }` envelope (except `render`)
|
||||
- `openStore()` is now async and auto-bootstraps
|
||||
|
||||
### New Features
|
||||
|
||||
- 18 `@output/*` schemas registered at bootstrap
|
||||
- `list --type <hash-or-alias>` command (replaces `schema list`)
|
||||
- `verify` now checks both hash integrity and schema validation
|
||||
- `wrapEnvelope()` helper exported from `@uncaged/json-cas`
|
||||
- `openStore()` in `@uncaged/json-cas-fs` — async, auto mkdir + bootstrap
|
||||
- Default LiquidJS templates for all output schemas
|
||||
- Pipe composition: any command output can be piped to `render -p`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.6.0
|
||||
- @uncaged/json-cas-fs@0.6.0
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@uncaged/cli-json-cas",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"json-cas": "./src/index.ts",
|
||||
@@ -8,10 +8,10 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.5.3",
|
||||
"@uncaged/json-cas-fs": "^0.5.3"
|
||||
"@uncaged/json-cas": "^0.6.0",
|
||||
"@uncaged/json-cas-fs": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
|
||||
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
|
||||
{
|
||||
"type": "ASE7K6A0HG8W9",
|
||||
"type": "2NHHHE9NR0FVT",
|
||||
"value": {
|
||||
"payload": {
|
||||
"age": 30,
|
||||
"name": "Alice",
|
||||
},
|
||||
"type": "7XX5H51CVD9H0",
|
||||
"type": "3K9ETAPGFPE32",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `
|
||||
{
|
||||
"type": "8E2M8H30BHXS8",
|
||||
"type": "BZ3TV0PTATA52",
|
||||
"value": "ok",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `
|
||||
"{
|
||||
"type": "4N5REDA48XYJP",
|
||||
"type": "18HZGJY02WN1K",
|
||||
"value": []
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `
|
||||
"{
|
||||
"type": "7124NEATTWYYY",
|
||||
"type": "9BVX9SHACFC5J",
|
||||
"value": [
|
||||
"ERARPP19YJT05"
|
||||
"A0QKG4ERMXSFG"
|
||||
]
|
||||
}"
|
||||
`;
|
||||
@@ -40,40 +40,40 @@ exports[`Phase 2: Schema Validation 2.3 put against non-existent schema hash fai
|
||||
|
||||
exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
|
||||
{
|
||||
"type": "AYHQD2YA9G667",
|
||||
"type": "EMX6HW4CECBGK",
|
||||
"value": {
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
|
||||
{
|
||||
"type": "BVW2SAJ8606EZ",
|
||||
"type": "F68GWJAT9H6HP",
|
||||
"value": {
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
|
||||
{
|
||||
"type": "3BY1S4RKNMR0P",
|
||||
"type": "A4TV1FVBBNG5M",
|
||||
"value": [
|
||||
{
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -81,14 +81,14 @@ exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
|
||||
|
||||
exports[`Phase 3: Variable System 3.4 var list prefix filters by prefix 1`] = `
|
||||
{
|
||||
"type": "3BY1S4RKNMR0P",
|
||||
"type": "A4TV1FVBBNG5M",
|
||||
"value": [
|
||||
{
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -96,48 +96,48 @@ exports[`Phase 3: Variable System 3.4 var list prefix filters by prefix 1`] = `
|
||||
|
||||
exports[`Phase 3: Variable System 3.5 var set upsert updates existing variable 1`] = `
|
||||
{
|
||||
"type": "AYHQD2YA9G667",
|
||||
"type": "EMX6HW4CECBGK",
|
||||
"value": {
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {},
|
||||
"value": "F68P1BZ46YDXM",
|
||||
"value": "FNDM9C6P23238",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.6 var tag adds kv tag and label 1`] = `
|
||||
{
|
||||
"type": "BKMJ3DJHTS6VB",
|
||||
"type": "92JS8RXGKDKC2",
|
||||
"value": {
|
||||
"labels": [
|
||||
"important",
|
||||
],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {
|
||||
"env": "prod",
|
||||
},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag 1`] = `
|
||||
{
|
||||
"type": "3BY1S4RKNMR0P",
|
||||
"type": "A4TV1FVBBNG5M",
|
||||
"value": [
|
||||
{
|
||||
"labels": [
|
||||
"important",
|
||||
],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {
|
||||
"env": "prod",
|
||||
},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -145,18 +145,18 @@ exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag
|
||||
|
||||
exports[`Phase 3: Variable System 3.8 var list --tag important filters by label 1`] = `
|
||||
{
|
||||
"type": "3BY1S4RKNMR0P",
|
||||
"type": "A4TV1FVBBNG5M",
|
||||
"value": [
|
||||
{
|
||||
"labels": [
|
||||
"important",
|
||||
],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {
|
||||
"env": "prod",
|
||||
},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -164,62 +164,62 @@ exports[`Phase 3: Variable System 3.8 var list --tag important filters by label
|
||||
|
||||
exports[`Phase 3: Variable System 3.9 var tag remove deletes label 1`] = `
|
||||
{
|
||||
"type": "BKMJ3DJHTS6VB",
|
||||
"type": "92JS8RXGKDKC2",
|
||||
"value": {
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {
|
||||
"env": "prod",
|
||||
},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
|
||||
{
|
||||
"type": "ASWN8JEGAG7AP",
|
||||
"type": "00YH4RTNVWTRB",
|
||||
"value": [
|
||||
{
|
||||
"labels": [],
|
||||
"name": "myapp/config",
|
||||
"schema": "7XX5H51CVD9H0",
|
||||
"schema": "3K9ETAPGFPE32",
|
||||
"tags": {
|
||||
"env": "prod",
|
||||
},
|
||||
"value": "ERARPP19YJT05",
|
||||
"value": "A0QKG4ERMXSFG",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=myapp/config, schema=7XX5H51CVD9H0"`;
|
||||
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=myapp/config, schema=3K9ETAPGFPE32"`;
|
||||
|
||||
exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
|
||||
{
|
||||
"type": "9YJZ09DDAYAWR",
|
||||
"type": "DTNQZ90QQHA6D",
|
||||
"value": {
|
||||
"contentHash": "FC8WACA792B6F",
|
||||
"schemaHash": "7XX5H51CVD9H0",
|
||||
"contentHash": "DBBPX5JJ74SWZ",
|
||||
"schemaHash": "3K9ETAPGFPE32",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 4: Template System 4.2 template get returns template text 1`] = `
|
||||
{
|
||||
"type": "FJG23DR9456WA",
|
||||
"type": "8KMGVC6TG12TS",
|
||||
"value": "Name: {{ payload.name }}, Age: {{ payload.age }}",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 4: Template System 4.3 template list shows registered templates 1`] = `
|
||||
{
|
||||
"type": "3JB2JHXHZG2Z1",
|
||||
"type": "6RZCT3XX4J8BC",
|
||||
"value": [
|
||||
{
|
||||
"contentHash": "FC8WACA792B6F",
|
||||
"schemaHash": "7XX5H51CVD9H0",
|
||||
"contentHash": "DBBPX5JJ74SWZ",
|
||||
"schemaHash": "3K9ETAPGFPE32",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -227,14 +227,14 @@ exports[`Phase 4: Template System 4.3 template list shows registered templates 1
|
||||
|
||||
exports[`Phase 4: Template System 4.4 template delete removes template 1`] = `
|
||||
{
|
||||
"type": "0PYGQE16XPM70",
|
||||
"type": "DBHJH385HGVTM",
|
||||
"value": {
|
||||
"deleted": true,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Phase 4: Template System 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: 7XX5H51CVD9H0"`;
|
||||
exports[`Phase 4: Template System 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: 3K9ETAPGFPE32"`;
|
||||
|
||||
exports[`Phase 5: Render 5.1 render fills payload variables 1`] = `"Hello Alice!"`;
|
||||
|
||||
@@ -254,13 +254,13 @@ command's @output/* schema (shown in parentheses); pipe any envelope into
|
||||
\`render -p\` to render its value (cas_ref hashes are expanded).
|
||||
|
||||
Commands:
|
||||
put <type-hash> <file.json> Store node, print envelope (value=hash) (@output/put)
|
||||
put <type-hash> <file.json|--pipe> Store node, print envelope (value=hash) (@output/put)
|
||||
get <hash> Print node as envelope (@output/get)
|
||||
has <hash> Print envelope (value=boolean) (@output/has)
|
||||
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@output/verify)
|
||||
refs <hash> List direct cas_ref edges (@output/refs)
|
||||
walk <hash> [--format tree] Recursive traversal (@output/walk)
|
||||
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
|
||||
hash <type-hash> <file.json|--pipe> Compute hash without storing (@output/hash)
|
||||
render <hash> [options] Render node as text with resolution decay (raw output)
|
||||
render --pipe/-p [options] Render { type, value } from stdin (raw output)
|
||||
list --type <hash-or-alias> List hashes for a type (value=string[]) (@output/list)
|
||||
@@ -285,5 +285,5 @@ Flags:
|
||||
--resolution <n> Initial resolution for render (default: 1.0)
|
||||
--decay <n> Decay factor for render (default: 0.5)
|
||||
--epsilon <n> Cutoff threshold for render (default: 0.01)
|
||||
--pipe, -p Read { type, value } JSON from stdin for render"
|
||||
--pipe, -p Read from stdin (put/hash: raw JSON payload; render: { type, value } envelope)"
|
||||
`;
|
||||
|
||||
@@ -224,6 +224,39 @@ describe("@ Alias Resolution - put", () => {
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("ucas put @schema with nested type constraints should succeed", async () => {
|
||||
await runCliAlias("init");
|
||||
|
||||
const schemaFile = join(testDir, "constrained-schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1, maxLength: 50 },
|
||||
age: { type: "number", minimum: 0, maximum: 150 },
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
}),
|
||||
);
|
||||
|
||||
const { stdout, stderr, exitCode } = await runCliAlias(
|
||||
"put",
|
||||
"@schema",
|
||||
schemaFile,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("@ Alias Resolution - hash", () => {
|
||||
|
||||
@@ -628,3 +628,68 @@ describe("Phase 8: Pipe Composition", () => {
|
||||
expect(stdout).toBe("Person: Carol (25)");
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Phase 9: Put/Hash Pipe Input ----
|
||||
|
||||
describe("Phase 9: Put/Hash Pipe Input", () => {
|
||||
test("9.1 put -p reads JSON from stdin and stores node", async () => {
|
||||
const payload = JSON.stringify({ name: "PipeAlice", age: 99 });
|
||||
const { stdout, exitCode } = await runCliWithStdin(
|
||||
["put", typeHash, "-p"],
|
||||
payload,
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
const hash = envValue(stdout);
|
||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
|
||||
// Verify stored correctly
|
||||
const { stdout: getOut } = await runCli(["get", hash as string]);
|
||||
expect(getOut).toContain("PipeAlice");
|
||||
});
|
||||
|
||||
test("9.2 hash -p reads JSON from stdin and computes hash without storing", async () => {
|
||||
const payload = JSON.stringify({ name: "PipeBob", age: 55 });
|
||||
const { stdout, exitCode } = await runCliWithStdin(
|
||||
["hash", typeHash, "-p"],
|
||||
payload,
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
const hash = envValue(stdout);
|
||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
|
||||
// Should NOT be stored
|
||||
const { exitCode: hasExit, stdout: hasOut } = await runCli([
|
||||
"has",
|
||||
hash as string,
|
||||
]);
|
||||
expect(hasExit).toBe(0);
|
||||
expect(envValue(hasOut)).toBe(false);
|
||||
});
|
||||
|
||||
test("9.3 put -p with file arg errors", async () => {
|
||||
const { stderr, exitCode } = await runCliWithStdin(
|
||||
["put", typeHash, "some-file.json", "-p"],
|
||||
"{}",
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Cannot use --pipe/-p with a file argument");
|
||||
});
|
||||
|
||||
test("9.4 put -p with empty stdin errors", async () => {
|
||||
const { stderr, exitCode } = await runCliWithStdin(
|
||||
["put", typeHash, "-p"],
|
||||
"",
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("No input on stdin");
|
||||
});
|
||||
|
||||
test("9.5 put -p with invalid JSON errors", async () => {
|
||||
const { stderr, exitCode } = await runCliWithStdin(
|
||||
["put", typeHash, "-p"],
|
||||
"not json",
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Invalid JSON on stdin");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getSchema,
|
||||
InvalidTagFormatError,
|
||||
InvalidVariableNameError,
|
||||
putSchema,
|
||||
refs,
|
||||
renderAsync,
|
||||
renderDirect,
|
||||
@@ -74,6 +75,8 @@ function parseArgs(argv: string[]): { flags: Flags; positional: string[] } {
|
||||
} else {
|
||||
flags[key] = true;
|
||||
}
|
||||
} else if (arg === "-p") {
|
||||
flags.p = true;
|
||||
} else {
|
||||
positional.push(arg);
|
||||
}
|
||||
@@ -112,6 +115,22 @@ function readJsonFile(file: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
async function readStdinJson(): Promise<unknown> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk as Buffer);
|
||||
}
|
||||
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
||||
if (!input) {
|
||||
die("No input on stdin. Pipe JSON content.");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return die("Invalid JSON on stdin.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the filesystem-backed CAS store.
|
||||
* Automatically creates directory and bootstraps if needed.
|
||||
@@ -179,14 +198,36 @@ function parseTagsLabels(args: string[]): {
|
||||
// ---- Commands ----
|
||||
|
||||
async function cmdPut(args: string[]): Promise<void> {
|
||||
const isPipe = flags.pipe === true || flags.p === true;
|
||||
const typeHashOrAlias = args[0];
|
||||
const file = args[1];
|
||||
if (!typeHashOrAlias || !file)
|
||||
die("Usage: json-cas put <type-hash> <file.json>");
|
||||
const file = isPipe ? undefined : args[1];
|
||||
if (!typeHashOrAlias || (!isPipe && !file))
|
||||
die(
|
||||
"Usage: json-cas put <type-hash> <file.json>\n json-cas put <type-hash> --pipe/-p",
|
||||
);
|
||||
if (isPipe && args[1])
|
||||
die("Cannot use --pipe/-p with a file argument. Use one or the other.");
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
||||
const payload = readJsonFile(file);
|
||||
const payload = isPipe ? await readStdinJson() : readJsonFile(file as string);
|
||||
const store = await openStore();
|
||||
|
||||
// Schema nodes: use putSchema() which validates via isValidSchema() (recursive)
|
||||
// instead of ajv against meta-schema (which can't express recursive constraints)
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
const metaHash = builtinSchemas["@schema"];
|
||||
if (typeHash === metaHash) {
|
||||
try {
|
||||
const hash = await putSchema(store, payload as Record<string, unknown>);
|
||||
out(await wrapEnvelope(store, "@output/put", hash));
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Validation failed: payload in ${file} does not match schema ${typeHash}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if schema exists
|
||||
const schema = getSchema(store, typeHash);
|
||||
if (schema === null) {
|
||||
@@ -293,12 +334,17 @@ async function cmdWalk(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
async function cmdHash(args: string[]): Promise<void> {
|
||||
const isPipe = flags.pipe === true || flags.p === true;
|
||||
const typeHashOrAlias = args[0];
|
||||
const file = args[1];
|
||||
if (!typeHashOrAlias || !file)
|
||||
die("Usage: json-cas hash <type-hash> <file.json>");
|
||||
const file = isPipe ? undefined : args[1];
|
||||
if (!typeHashOrAlias || (!isPipe && !file))
|
||||
die(
|
||||
"Usage: json-cas hash <type-hash> <file.json>\n json-cas hash <type-hash> --pipe/-p",
|
||||
);
|
||||
if (isPipe && args[1])
|
||||
die("Cannot use --pipe/-p with a file argument. Use one or the other.");
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
||||
const payload = readJsonFile(file);
|
||||
const payload = isPipe ? await readStdinJson() : readJsonFile(file as string);
|
||||
const hash = await computeHash(typeHash, payload);
|
||||
const store = await openStore();
|
||||
out(await wrapEnvelope(store, "@output/hash", hash));
|
||||
@@ -786,13 +832,13 @@ command's @output/* schema (shown in parentheses); pipe any envelope into
|
||||
\`render -p\` to render its value (cas_ref hashes are expanded).
|
||||
|
||||
Commands:
|
||||
put <type-hash> <file.json> Store node, print envelope (value=hash) (@output/put)
|
||||
put <type-hash> <file.json|--pipe> Store node, print envelope (value=hash) (@output/put)
|
||||
get <hash> Print node as envelope (@output/get)
|
||||
has <hash> Print envelope (value=boolean) (@output/has)
|
||||
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@output/verify)
|
||||
refs <hash> List direct cas_ref edges (@output/refs)
|
||||
walk <hash> [--format tree] Recursive traversal (@output/walk)
|
||||
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
|
||||
hash <type-hash> <file.json|--pipe> Compute hash without storing (@output/hash)
|
||||
render <hash> [options] Render node as text with resolution decay (raw output)
|
||||
render --pipe/-p [options] Render { type, value } from stdin (raw output)
|
||||
list --type <hash-or-alias> List hashes for a type (value=string[]) (@output/list)
|
||||
@@ -817,7 +863,7 @@ Flags:
|
||||
--resolution <n> Initial resolution for render (default: 1.0)
|
||||
--decay <n> Decay factor for render (default: 0.5)
|
||||
--epsilon <n> Cutoff threshold for render (default: 0.01)
|
||||
--pipe, -p Read { type, value } JSON from stdin for render`);
|
||||
--pipe, -p Read from stdin (put/hash: raw JSON payload; render: { type, value } envelope)`);
|
||||
}
|
||||
|
||||
// ---- Dispatch ----
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# @uncaged/json-cas-fs
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Unified `{ type, value }` envelope output for all CLI commands (RFC [#67](https://github.com/uncaged/json-cas/issues/67)).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `init`, `bootstrap`, `cat`, `schema put/get/list/validate` commands
|
||||
- All CLI command outputs now wrapped in `{ type, value }` envelope (except `render`)
|
||||
- `openStore()` is now async and auto-bootstraps
|
||||
|
||||
### New Features
|
||||
|
||||
- 18 `@output/*` schemas registered at bootstrap
|
||||
- `list --type <hash-or-alias>` command (replaces `schema list`)
|
||||
- `verify` now checks both hash integrity and schema validation
|
||||
- `wrapEnvelope()` helper exported from `@uncaged/json-cas`
|
||||
- `openStore()` in `@uncaged/json-cas-fs` — async, auto mkdir + bootstrap
|
||||
- Default LiquidJS templates for all output schemas
|
||||
- Pipe composition: any command output can be piped to `render -p`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.6.0
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@uncaged/json-cas-fs",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -16,10 +16,10 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.5.3",
|
||||
"@uncaged/json-cas": "^0.6.0",
|
||||
"cborg": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @uncaged/json-cas
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Unified `{ type, value }` envelope output for all CLI commands (RFC [#67](https://github.com/uncaged/json-cas/issues/67)).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `init`, `bootstrap`, `cat`, `schema put/get/list/validate` commands
|
||||
- All CLI command outputs now wrapped in `{ type, value }` envelope (except `render`)
|
||||
- `openStore()` is now async and auto-bootstraps
|
||||
|
||||
### New Features
|
||||
|
||||
- 18 `@output/*` schemas registered at bootstrap
|
||||
- `list --type <hash-or-alias>` command (replaces `schema list`)
|
||||
- `verify` now checks both hash integrity and schema validation
|
||||
- `wrapEnvelope()` helper exported from `@uncaged/json-cas`
|
||||
- `openStore()` in `@uncaged/json-cas-fs` — async, auto mkdir + bootstrap
|
||||
- Default LiquidJS templates for all output schemas
|
||||
- Pipe composition: any command output can be piped to `render -p`
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@uncaged/json-cas",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -16,7 +16,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.20.0",
|
||||
|
||||
@@ -60,6 +60,17 @@ const BOOTSTRAP_PAYLOAD = {
|
||||
enum: { type: "array" },
|
||||
const: {},
|
||||
description: { type: "string" },
|
||||
// P1 leaf constraints
|
||||
minimum: { type: "number" },
|
||||
maximum: { type: "number" },
|
||||
exclusiveMinimum: { type: "number" },
|
||||
exclusiveMaximum: { type: "number" },
|
||||
minLength: { type: "number" },
|
||||
maxLength: { type: "number" },
|
||||
pattern: { type: "string" },
|
||||
minItems: { type: "number" },
|
||||
maxItems: { type: "number" },
|
||||
uniqueItems: { type: "boolean" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -388,6 +388,107 @@ describe("bootstrap meta-schema self-reference", () => {
|
||||
expect(dataNode.type).not.toBe(metaHash);
|
||||
});
|
||||
|
||||
// ── P1 leaf constraints ──────────────────────────────────────────────────
|
||||
|
||||
test("accepts schema with numeric constraints (minimum/maximum)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hash = await putSchema(store, {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 100,
|
||||
exclusiveMinimum: -1,
|
||||
exclusiveMaximum: 101,
|
||||
});
|
||||
expect(hash).toHaveLength(13);
|
||||
|
||||
// validate a conforming payload
|
||||
const nodeHash = await store.put(hash, 42);
|
||||
const node = store.get(nodeHash) as CasNode;
|
||||
expect(validate(store, node)).toBe(true);
|
||||
|
||||
// validate a non-conforming payload
|
||||
const badHash = await store.put(hash, 200);
|
||||
const badNode = store.get(badHash) as CasNode;
|
||||
expect(validate(store, badNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hash = await putSchema(store, {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 10,
|
||||
pattern: "^[a-z]+$",
|
||||
});
|
||||
expect(hash).toHaveLength(13);
|
||||
|
||||
const goodHash = await store.put(hash, "hello");
|
||||
expect(validate(store, store.get(goodHash) as CasNode)).toBe(true);
|
||||
|
||||
const badHash = await store.put(hash, "HELLO");
|
||||
expect(validate(store, store.get(badHash) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hash = await putSchema(store, {
|
||||
type: "array",
|
||||
items: { type: "number" },
|
||||
minItems: 1,
|
||||
maxItems: 3,
|
||||
uniqueItems: true,
|
||||
});
|
||||
expect(hash).toHaveLength(13);
|
||||
|
||||
const goodHash = await store.put(hash, [1, 2, 3]);
|
||||
expect(validate(store, store.get(goodHash) as CasNode)).toBe(true);
|
||||
|
||||
const tooMany = await store.put(hash, [1, 2, 3, 4]);
|
||||
expect(validate(store, store.get(tooMany) as CasNode)).toBe(false);
|
||||
|
||||
const dupes = await store.put(hash, [1, 1]);
|
||||
expect(validate(store, store.get(dupes) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects schema with wrong constraint types", async () => {
|
||||
const store = createMemoryStore();
|
||||
await expect(
|
||||
putSchema(store, { type: "number", minimum: "zero" } as never),
|
||||
).rejects.toThrow();
|
||||
await expect(
|
||||
putSchema(store, { type: "string", maxLength: true } as never),
|
||||
).rejects.toThrow();
|
||||
await expect(
|
||||
putSchema(store, { type: "array", uniqueItems: 1 } as never),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("accepts schema with nested property constraints", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hash = await putSchema(store, {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1, maxLength: 50 },
|
||||
age: { type: "number", minimum: 0, maximum: 150 },
|
||||
scores: {
|
||||
type: "array",
|
||||
items: { type: "number" },
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
});
|
||||
expect(hash).toHaveLength(13);
|
||||
|
||||
const good = await store.put(hash, {
|
||||
name: "Alice",
|
||||
age: 30,
|
||||
scores: [95, 87],
|
||||
});
|
||||
expect(validate(store, store.get(good) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("bootstrap is idempotent across putSchema calls", async () => {
|
||||
const store = createMemoryStore();
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
|
||||
@@ -35,6 +35,17 @@ const ALLOWED_SCHEMA_KEYS = new Set([
|
||||
"enum",
|
||||
"const",
|
||||
"description",
|
||||
// P1 leaf constraints (no collectRefs impact)
|
||||
"minimum",
|
||||
"maximum",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"pattern",
|
||||
"minItems",
|
||||
"maxItems",
|
||||
"uniqueItems",
|
||||
]);
|
||||
|
||||
const JSON_SCHEMA_TYPES = new Set([
|
||||
@@ -126,6 +137,29 @@ function isValidSchema(value: unknown): boolean {
|
||||
if (!Array.isArray(schema.enum) || schema.enum.length === 0) return false;
|
||||
}
|
||||
|
||||
// P1 leaf constraints — type checks only
|
||||
if ("minimum" in schema && typeof schema.minimum !== "number") return false;
|
||||
if ("maximum" in schema && typeof schema.maximum !== "number") return false;
|
||||
if (
|
||||
"exclusiveMinimum" in schema &&
|
||||
typeof schema.exclusiveMinimum !== "number"
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
"exclusiveMaximum" in schema &&
|
||||
typeof schema.exclusiveMaximum !== "number"
|
||||
)
|
||||
return false;
|
||||
if ("minLength" in schema && typeof schema.minLength !== "number")
|
||||
return false;
|
||||
if ("maxLength" in schema && typeof schema.maxLength !== "number")
|
||||
return false;
|
||||
if ("pattern" in schema && typeof schema.pattern !== "string") return false;
|
||||
if ("minItems" in schema && typeof schema.minItems !== "number") return false;
|
||||
if ("maxItems" in schema && typeof schema.maxItems !== "number") return false;
|
||||
if ("uniqueItems" in schema && typeof schema.uniqueItems !== "boolean")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Prevent publishing packages that still reference workspace:* dependencies
|
||||
if grep -q '"workspace:' package.json 2>/dev/null; then
|
||||
echo "❌ Found workspace:* dependencies in package.json — cannot publish directly."
|
||||
echo " Use 'changeset publish' which resolves workspace protocol automatically."
|
||||
grep '"workspace:' package.json
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user