From 4e002572ae8f9b7a052d4373947549ac4a4f2496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 1 Jun 2026 12:00:20 +0000 Subject: [PATCH] feat: all hash params accept variable names All commands (get, has, verify, refs, walk, list --type, var --schema) now resolve variable names via resolveHash(). Added 7 e2e tests. Also fixed isHash to use Crockford Base32 charset. Fixes #19 --- packages/cli/src/index.ts | 241 ++++++++++++++++------------ packages/cli/tests/alias.test.ts | 94 +++++++++++ packages/cli/tests/variable.test.ts | 12 +- 3 files changed, 241 insertions(+), 106 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 46050a8..a87ed80 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -178,7 +178,7 @@ async function openStoreAndVarStore(): Promise<{ * Hash format check: 13-char uppercase Crockford Base32. */ function isHash(input: string): boolean { - return /^[0-9A-Z]{13}$/.test(input); + return /^[0-9A-HJKMNP-TV-Z]{13}$/.test(input); } /** @@ -193,7 +193,7 @@ function resolveHash(input: string, varStore: VariableStore): Hash { const variants = varStore.list({ exactName: input }); const first = variants[0]; if (!first) { - die(`Schema not found: ${input}`); + die(`Error: Schema not found: ${input}`); } return first.value as Hash; } @@ -289,93 +289,118 @@ async function cmdPut(args: string[]): Promise { } async function cmdGet(args: string[]): Promise { - const hash = args[0]; - if (!hash) die("Usage: ocas get "); - const store = await openStore(); - const node = store.get(hash); - if (node === null) die(`Node not found: ${hash}`); - await out(await wrapEnvelope(store, "@ocas/output/get", node), store); + const input = args[0]; + if (!input) die("Usage: ocas get "); + const { store, varStore } = await openStoreAndVarStore(); + try { + const hash = resolveHash(input, varStore); + const node = store.get(hash); + if (node === null) die(`Node not found: ${hash}`); + await out(await wrapEnvelope(store, "@ocas/output/get", node), store); + } finally { + varStore.close(); + } } async function cmdHas(args: string[]): Promise { - const hash = args[0]; - if (!hash) die("Usage: ocas has "); - const store = await openStore(); - await out( - await wrapEnvelope(store, "@ocas/output/has", store.has(hash)), - store, - ); + const input = args[0]; + if (!input) die("Usage: ocas has "); + const { store, varStore } = await openStoreAndVarStore(); + try { + const hash = resolveHash(input, varStore); + await out( + await wrapEnvelope(store, "@ocas/output/has", store.has(hash)), + store, + ); + } finally { + varStore.close(); + } } async function cmdVerify(args: string[]): Promise { - const hash = args[0]; - if (!hash) die("Usage: ocas verify "); - const store = await openStore(); - const node = store.get(hash); - if (node === null) die(`Node not found: ${hash}`); - const ok = await verify(hash, node); - let status: string; - if (!ok) { - status = "corrupted"; - } else { - status = validate(store, node) ? "ok" : "invalid"; + const input = args[0]; + if (!input) die("Usage: ocas verify "); + const { store, varStore } = await openStoreAndVarStore(); + try { + const hash = resolveHash(input, varStore); + const node = store.get(hash); + if (node === null) die(`Node not found: ${hash}`); + const ok = await verify(hash, node); + let status: string; + if (!ok) { + status = "corrupted"; + } else { + status = validate(store, node) ? "ok" : "invalid"; + } + await out(await wrapEnvelope(store, "@ocas/output/verify", status), store); + } finally { + varStore.close(); } - await out(await wrapEnvelope(store, "@ocas/output/verify", status), store); } async function cmdRefs(args: string[]): Promise { - const hash = args[0]; - if (!hash) die("Usage: ocas refs "); - const store = await openStore(); - const node = store.get(hash); - if (node === null) die(`Node not found: ${hash}`); - const refHashes = refs(store, node); - await out(await wrapEnvelope(store, "@ocas/output/refs", refHashes), store); + const input = args[0]; + if (!input) die("Usage: ocas refs "); + const { store, varStore } = await openStoreAndVarStore(); + try { + const hash = resolveHash(input, varStore); + const node = store.get(hash); + if (node === null) die(`Node not found: ${hash}`); + const refHashes = refs(store, node); + await out(await wrapEnvelope(store, "@ocas/output/refs", refHashes), store); + } finally { + varStore.close(); + } } async function cmdWalk(args: string[]): Promise { - const hash = args[0]; - if (!hash) die("Usage: ocas walk [--format tree]"); - const store = await openStore(); - const format = flags.format; + const input = args[0]; + if (!input) die("Usage: ocas walk [--format tree]"); + const { store, varStore } = await openStoreAndVarStore(); + try { + const hash = resolveHash(input, varStore); + const format = flags.format; - if (format === "tree") { - const childMap = new Map(); - walk(store, hash, (h, node) => { - childMap.set(h, refs(store, node)); - }); + if (format === "tree") { + const childMap = new Map(); + walk(store, hash, (h, node) => { + childMap.set(h, refs(store, node)); + }); - const printed = new Set(); - const lines: string[] = []; + const printed = new Set(); + const lines: string[] = []; - function printNode(h: Hash, prefix: string, isLast: boolean): void { - const connector = prefix === "" ? "" : isLast ? "└── " : "├── "; - if (printed.has(h)) { - lines.push(`${prefix}${connector}${h} (seen)`); - return; + function printNode(h: Hash, prefix: string, isLast: boolean): void { + const connector = prefix === "" ? "" : isLast ? "└── " : "├── "; + if (printed.has(h)) { + lines.push(`${prefix}${connector}${h} (seen)`); + return; + } + printed.add(h); + lines.push(`${prefix}${connector}${h}`); + + const kids = childMap.get(h) ?? []; + const childPrefix = + prefix === "" ? "" : prefix + (isLast ? " " : "│ "); + for (let i = 0; i < kids.length; i++) { + printNode(kids[i] as Hash, childPrefix, i === kids.length - 1); + } } - printed.add(h); - lines.push(`${prefix}${connector}${h}`); - const kids = childMap.get(h) ?? []; - const childPrefix = - prefix === "" ? "" : prefix + (isLast ? " " : "│ "); - for (let i = 0; i < kids.length; i++) { - printNode(kids[i] as Hash, childPrefix, i === kids.length - 1); - } + printNode(hash, "", true); + await out( + await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n")), + store, + ); + } else { + const hashes: Hash[] = []; + walk(store, hash, (h) => { + hashes.push(h); + }); + await out(await wrapEnvelope(store, "@ocas/output/walk", hashes), store); } - - printNode(hash, "", true); - await out( - await wrapEnvelope(store, "@ocas/output/walk", lines.join("\n")), - store, - ); - } else { - const hashes: Hash[] = []; - walk(store, hash, (h) => { - hashes.push(h); - }); - await out(await wrapEnvelope(store, "@ocas/output/walk", hashes), store); + } finally { + varStore.close(); } } @@ -404,25 +429,26 @@ async function cmdHash(args: string[]): Promise { async function cmdRender(args: string[]): Promise { const isPipe = flags.pipe === true || flags.p === true; - const hash = args[0]; + const input = args[0]; - if (isPipe && hash) { + if (isPipe && input) { die("Cannot use --pipe/-p with a hash argument. Use one or the other."); } - if (!isPipe && !hash) { + if (!isPipe && !input) { die( - "Usage: ocas render [--resolution ] [--decay ] [--epsilon ]\n ocas render --pipe/-p [--resolution ] [--decay ] [--epsilon ]", + "Usage: ocas render [--resolution ] [--decay ] [--epsilon ]\n ocas render --pipe/-p [--resolution ] [--decay ] [--epsilon ]", ); } - let storeAndVarStore: { store: Store; varStore: VariableStore } | undefined; let store: Store; + let varStore: VariableStore | undefined; if (isPipe) { store = await openStore(); } else { - storeAndVarStore = await openStoreAndVarStore(); - store = storeAndVarStore.store; + const opened = await openStoreAndVarStore(); + store = opened.store; + varStore = opened.varStore; } // Parse numeric options @@ -480,7 +506,7 @@ async function cmdRender(args: string[]): Promise { } // Validate type hash format: 13-char uppercase Crockford Base32 - if (!/^[0-9A-Z]{13}$/.test(envelope.type)) { + if (!isHash(envelope.type)) { die( `Invalid type hash: "${envelope.type}". Expected 13-character uppercase Crockford Base32 string.`, ); @@ -498,10 +524,11 @@ async function cmdRender(args: string[]): Promise { ); process.stdout.write(output); } else { - const varStore = ( - storeAndVarStore as { store: Store; varStore: VariableStore } - ).varStore; + if (varStore === undefined) { + die("Internal error: varStore not initialized"); + } try { + const hash = resolveHash(input as string, varStore); const output = await renderAsync(store, hash, { resolution, decay, @@ -586,16 +613,17 @@ async function cmdVarSet(args: string[]): Promise { async function cmdVarGet(args: string[]): Promise { const name = args[0]; - const schema = flags.schema as string | undefined; + const schemaInput = flags.schema as string | undefined; - if (!name || !schema) { - die("Usage: ocas var get --schema "); + if (!name || !schemaInput) { + die("Usage: ocas var get --schema "); } const store = await openStore(); const varStore = createVariableStore(resolve(varDbPath), store); try { + const schema = resolveHash(schemaInput, varStore); const variable = varStore.get(name, schema); if (variable === null) { die(`Error: Variable not found: name=${name}, schema=${schema}`); @@ -611,10 +639,10 @@ async function cmdVarGet(args: string[]): Promise { async function cmdVarDelete(args: string[]): Promise { const name = args[0]; - const schema = flags.schema as string | undefined; + const schemaInput = flags.schema as string | undefined; if (!name) { - die("Usage: ocas var delete [--schema ]"); + die("Usage: ocas var delete [--schema ]"); } if (name.startsWith("@ocas/")) { @@ -625,7 +653,8 @@ async function cmdVarDelete(args: string[]): Promise { const varStore = createVariableStore(resolve(varDbPath), store); try { - if (schema !== undefined) { + if (schemaInput !== undefined) { + const schema = resolveHash(schemaInput, varStore); // Precise deletion: remove specific (name, schema) variant const variable = varStore.remove(name, schema); await out( @@ -652,21 +681,22 @@ async function cmdVarDelete(args: string[]): Promise { async function cmdVarTag(args: string[]): Promise { const name = args[0]; - const schema = flags.schema as string | undefined; + const schemaInput = flags.schema as string | undefined; - if (!name || !schema) { - die("Usage: ocas var tag --schema "); + if (!name || !schemaInput) { + die("Usage: ocas var tag --schema "); } const tagArgs = args.slice(1); if (tagArgs.length === 0) { - die("Usage: ocas var tag --schema "); + die("Usage: ocas var tag --schema "); } const store = await openStore(); const varStore = createVariableStore(resolve(varDbPath), store); try { + const schema = resolveHash(schemaInput, varStore); const { tags, labels, deleteNames } = parseTagsLabels(tagArgs); const variable = varStore.tag(name, schema, { @@ -695,13 +725,17 @@ async function cmdVarTag(args: string[]): Promise { async function cmdVarList(args: string[]): Promise { const namePrefix = args[0] ?? ""; - const schema = flags.schema as string | undefined; + const schemaInput = flags.schema as string | undefined; const tagFlags = flags.tag; const store = await openStore(); const varStore = createVariableStore(resolve(varDbPath), store); try { + const schema = + schemaInput !== undefined + ? resolveHash(schemaInput, varStore) + : undefined; // Parse tags/labels from --tag flags const tagArgs = Array.isArray(tagFlags) ? tagFlags @@ -736,16 +770,19 @@ async function cmdVarList(args: string[]): Promise { } async function cmdTemplateSet(args: string[]): Promise { - const schemaHash = args[0]; + const schemaInput = args[0]; const inlineFlag = flags.inline; - if (!schemaHash) { - die("Usage: ocas template set | --inline "); + if (!schemaInput) { + die( + "Usage: ocas template set | --inline ", + ); } const { store, varStore } = await openStoreAndVarStore(); try { + const schemaHash = resolveHash(schemaInput, varStore); // Validate schema hash exists in CAS if (!store.has(schemaHash)) { die(`Error: Schema hash not found in CAS: ${schemaHash}`); @@ -806,15 +843,16 @@ async function cmdTemplateSet(args: string[]): Promise { } async function cmdTemplateGet(args: string[]): Promise { - const schemaHash = args[0]; + const schemaInput = args[0]; - if (!schemaHash) { - die("Usage: ocas template get "); + if (!schemaInput) { + die("Usage: ocas template get "); } const { store, varStore } = await openStoreAndVarStore(); try { + const schemaHash = resolveHash(schemaInput, varStore); const varName = `@ocas/template/text/${schemaHash}`; const stringHash = resolveHash("@ocas/string", varStore); const variable = varStore.get(varName, stringHash); @@ -867,15 +905,16 @@ async function cmdTemplateList(_args: string[]): Promise { } async function cmdTemplateDelete(args: string[]): Promise { - const schemaHash = args[0]; + const schemaInput = args[0]; - if (!schemaHash) { - die("Usage: ocas template delete "); + if (!schemaInput) { + die("Usage: ocas template delete "); } const { store, varStore } = await openStoreAndVarStore(); try { + const schemaHash = resolveHash(schemaInput, varStore); const varName = `@ocas/template/text/${schemaHash}`; const stringHash = resolveHash("@ocas/string", varStore); varStore.remove(varName, stringHash); diff --git a/packages/cli/tests/alias.test.ts b/packages/cli/tests/alias.test.ts index 7fd63be..1d6ac27 100644 --- a/packages/cli/tests/alias.test.ts +++ b/packages/cli/tests/alias.test.ts @@ -185,3 +185,97 @@ describe("@ Alias Resolution - hash", () => { expect(envValue(stdout)).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); }); }); + +describe("@ Alias Resolution - hash params (Phase 3)", () => { + test("ocas get @ocas/string should resolve name to hash", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "get", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + const value = envValue(stdout) as { type: string; payload: unknown }; + expect(value).toHaveProperty("type"); + expect(value).toHaveProperty("payload"); + }); + + test("ocas has @ocas/string should resolve name and return true", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "has", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(envValue(stdout)).toBe(true); + }); + + test("ocas verify @ocas/string should resolve name", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "verify", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(envValue(stdout)).toBe("ok"); + }); + + test("ocas refs @ocas/string should resolve name", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "refs", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(Array.isArray(envValue(stdout))).toBe(true); + }); + + test("ocas walk @ocas/string should resolve name", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "walk", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + const value = envValue(stdout); + expect(Array.isArray(value)).toBe(true); + expect((value as string[]).length).toBeGreaterThan(0); + }); + + test("ocas list --type @ocas/string should resolve name", async () => { + await runCliAlias("init"); + + const { stdout, stderr, exitCode } = await runCliAlias( + "list", + "--type", + "@ocas/string", + ); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(Array.isArray(envValue(stdout))).toBe(true); + }); + + test("ocas get with non-existent name should fail with Error", async () => { + await runCliAlias("init"); + + const { stderr, exitCode } = await runCliAlias("get", "@nonexistent/name"); + + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Error: Schema not found:"); + }); +}); diff --git a/packages/cli/tests/variable.test.ts b/packages/cli/tests/variable.test.ts index 266edef..02f2914 100644 --- a/packages/cli/tests/variable.test.ts +++ b/packages/cli/tests/variable.test.ts @@ -347,7 +347,9 @@ describe("var get", () => { const { stderr, exitCode } = await runCli("var", "get", "config"); expect(exitCode).toBe(1); - expect(stderr).toContain("Usage: ocas var get --schema "); + expect(stderr).toContain( + "Usage: ocas var get --schema ", + ); }); test("distinguish variants by schema", async () => { @@ -454,12 +456,12 @@ describe("var delete", () => { "delete", "config", "--schema", - "NONEXISTENT_SCHEMA", + "00000000000ZZ", ); expect(exitCode).toBe(1); expect(stderr).toContain( - "Error: Variable not found: name=config, schema=NONEXISTENT_SCHEMA", + "Error: Variable not found: name=config, schema=00000000000ZZ", ); }); @@ -904,7 +906,7 @@ describe("var tag", () => { expect(exitCode).toBe(1); expect(stderr).toContain( - "Usage: ocas var tag --schema ", + "Usage: ocas var tag --schema ", ); }); @@ -922,7 +924,7 @@ describe("var tag", () => { expect(exitCode).toBe(1); expect(stderr).toContain( - "Usage: ocas var tag --schema ", + "Usage: ocas var tag --schema ", ); }); }); -- 2.43.0