Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d19707e7b | |||
| 20922d92c2 | |||
| b22da63bf8 | |||
| 45f0d83af3 | |||
| 56f003077a | |||
| 52ebd9ed8e | |||
| ec6db5f494 | |||
| 5d54c0988c | |||
| fbcd1b5b0e | |||
| 191926428a | |||
| 077537ea5b | |||
| 84386f8a63 | |||
| eebdeb23da | |||
| 350ee3d7b5 | |||
| 02a364eb0b | |||
| 8176a228b2 | |||
| 2952b953b3 | |||
| 1554fbc719 | |||
| 3d903eaff8 | |||
| d8f2abbe12 | |||
| 0919f71f7e | |||
| 944f6c3254 | |||
| 6bc767d37e | |||
| efb0a0e721 | |||
| 5f544c019f | |||
| ab1e6df774 | |||
| b43bbef80f | |||
| b687ff82b3 | |||
| 04433e81ab | |||
| c9829e70ee | |||
| b19f38c41a | |||
| 52072a0065 | |||
| ec9e04c5c6 | |||
| 558a70fc04 | |||
| 656c780270 | |||
| 307fa032ad | |||
| 46d7991002 |
@@ -98,42 +98,65 @@ This is resolved to real version numbers only during publishing (see below).
|
||||
|
||||
## Release Process
|
||||
|
||||
Releases use a **release branch** workflow. `main` always keeps `workspace:*` for
|
||||
internal dependencies; version numbers are only fixed on the release branch.
|
||||
Releases use a **release branch** workflow with three phases: prepare → candidate → finalize.
|
||||
|
||||
### Prepare
|
||||
|
||||
```bash
|
||||
./scripts/prepare-release.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Checks you're on `main` with a clean tree and pending changesets
|
||||
2. Creates `release/<version>` branch
|
||||
3. Runs `changeset version` to fix versions and generate CHANGELOGs
|
||||
4. Runs full validation (install, build, lint, test)
|
||||
5. Commits the version bump
|
||||
|
||||
After preparation, review changes and fix any issues on the release branch.
|
||||
|
||||
### Publish
|
||||
|
||||
```bash
|
||||
./scripts/publish.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Validates you're on a `release/*` branch with no pending changesets
|
||||
2. Runs final build + test
|
||||
3. Publishes packages in order: `@ocas/core` → `@ocas/fs` → `@ocas/cli`
|
||||
4. Tags, pushes, merges back to `main`, cleans up the release branch
|
||||
`main` always keeps `workspace:*` for internal deps; release branches fix them to real versions.
|
||||
Changeset files are **only consumed once** during finalize — prerelease (rc) never touches them.
|
||||
|
||||
### Adding a Changeset
|
||||
|
||||
Before releasing, add changesets for your changes:
|
||||
Add changesets alongside feature PRs on `main`:
|
||||
|
||||
```bash
|
||||
bunx changeset # interactive — pick packages + bump type + summary
|
||||
```markdown
|
||||
<!-- .changeset/my-change.md -->
|
||||
---
|
||||
"@ocas/cli": patch
|
||||
---
|
||||
|
||||
Description of the change
|
||||
```
|
||||
|
||||
Changesets live in `.changeset/` as markdown files until consumed by `prepare-release.sh`.
|
||||
Changesets live in `.changeset/` as markdown files. Bump types: `patch` / `minor` / `major`.
|
||||
One changeset can cover multiple packages.
|
||||
|
||||
### Phase 1: Prepare (cut release branch)
|
||||
|
||||
- **Precondition:** on `main`, clean tree, `.changeset/` has pending changesets
|
||||
- **Steps:**
|
||||
1. Determine target version (from changeset bump types or manually)
|
||||
2. `git checkout -b release/<version>`
|
||||
3. Fix `workspace:*` → real version numbers in all `package.json`
|
||||
4. Commit
|
||||
- **Does NOT** run `changeset version`, does NOT write CHANGELOG
|
||||
|
||||
### Phase 2: Candidate (publish rc for validation)
|
||||
|
||||
- **Precondition:** on `release/*` branch
|
||||
- **Steps:**
|
||||
1. Set version to `<version>-rc.N` (first time rc.1, increment on subsequent runs)
|
||||
2. `bun install && bun run build && bun test && bun run check`
|
||||
3. Publish: `bun publish --tag rc` (order: core → fs → cli)
|
||||
4. Commit + push
|
||||
- **Repeatable:** fix bugs → add new changesets on the release branch → rc.N+1
|
||||
- **Does NOT** consume changesets, does NOT write CHANGELOG
|
||||
- Install for testing: `bun add -g @ocas/cli@rc`
|
||||
|
||||
### Phase 3: Finalize (official release)
|
||||
|
||||
- **Precondition:** on `release/*` branch, rc validated
|
||||
- **Steps:**
|
||||
1. Consume all `.changeset/*.md` → write CHANGELOG entries (use `changeset version` or manual)
|
||||
2. Set final version `<version>` (remove `-rc.N`)
|
||||
3. `bun install && bun run build && bun test && bun run check`
|
||||
4. Publish: `bun publish --tag latest` (order: core → fs → cli)
|
||||
5. Git tag `v<version>`
|
||||
6. Merge back to `main` (CHANGELOG comes along)
|
||||
7. Restore `workspace:*` on `main`
|
||||
8. Delete release branch
|
||||
|
||||
### Key Rules
|
||||
|
||||
- **Publish order** is always `@ocas/core` → `@ocas/fs` → `@ocas/cli`
|
||||
- **`workspace:*`** must be fixed before any publish — `bun publish` does NOT auto-replace them
|
||||
- **CHANGELOG** only contains official releases, never rc entries
|
||||
- **Changesets added on release branch** (bug fixes during rc) are consumed together at finalize
|
||||
|
||||
@@ -15,18 +15,18 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@ocas/cli",
|
||||
"version": "0.6.0",
|
||||
"version": "0.1.1",
|
||||
"bin": {
|
||||
"ocas": "src/index.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.6.0",
|
||||
"@ocas/fs": "^0.6.0",
|
||||
"@ocas/core": "0.1.1",
|
||||
"@ocas/fs": "0.1.1",
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@ocas/core",
|
||||
"version": "0.6.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"ajv": "^8.20.0",
|
||||
"cborg": "^4.2.3",
|
||||
@@ -36,9 +36,9 @@
|
||||
},
|
||||
"packages/fs": {
|
||||
"name": "@ocas/fs",
|
||||
"version": "0.6.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.6.0",
|
||||
"@ocas/core": "0.1.1",
|
||||
"cborg": "^4.2.3",
|
||||
},
|
||||
},
|
||||
|
||||
+17
-52
@@ -1,63 +1,28 @@
|
||||
# @uncaged/cli-json-cas
|
||||
# @ocas/cli
|
||||
|
||||
## 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.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.6.0
|
||||
- @uncaged/json-cas-fs@0.6.0
|
||||
- Fix render output missing trailing newline.
|
||||
- Fix TypeScript LSP errors: tsconfig `noEmit`, `exactOptionalPropertyTypes` conditional spread, `schemaHash` scope.
|
||||
- Add agent skill setup hint with version to help output. Remove postinstall script (blocked by bun security policy). Update `ocas prompt setup` to guide cleanup of old skill versions before installing new ones.
|
||||
|
||||
## 0.5.3
|
||||
- Updated dependencies:
|
||||
- @ocas/core@0.1.2
|
||||
- @ocas/fs@0.1.2
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.5.3
|
||||
- @uncaged/json-cas-fs@0.5.3
|
||||
- Add `ocas prompt usage` and `ocas prompt setup` commands for agent skill management. Prompt content is bundled with the CLI and versioned with it.
|
||||
- Add `--version` flag to display CLI version.
|
||||
|
||||
## 0.3.0
|
||||
- Updated dependencies:
|
||||
- @ocas/core@0.1.1
|
||||
- @ocas/fs@0.1.1
|
||||
|
||||
### Patch Changes
|
||||
## 0.1.0
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.3.0
|
||||
- @uncaged/json-cas-fs@0.3.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.2.0
|
||||
- @uncaged/json-cas-fs@0.2.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: replace workspace:^ with actual version numbers in published dependencies
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.1.3
|
||||
- @uncaged/json-cas-fs@0.1.3
|
||||
Initial release as `@ocas/cli`. CLI tool for OCAS with put, get, list, render, verify, gc, var, and template commands. Envelope output format with pipe composition support.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ocas/cli",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"ocas": "src/index.ts"
|
||||
@@ -10,8 +10,8 @@
|
||||
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "0.1.0",
|
||||
"@ocas/fs": "0.1.0"
|
||||
"@ocas/core": "0.1.2",
|
||||
"@ocas/fs": "0.1.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
+78
-40
@@ -92,6 +92,14 @@ function parseArgs(argv: string[]): { flags: Flags; positional: string[] } {
|
||||
|
||||
const { flags, positional } = parseArgs(process.argv.slice(2));
|
||||
|
||||
// --- Handle --version early ---
|
||||
if (flags.version === true) {
|
||||
const pkgPath = join(import.meta.dir, "..", "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
process.stdout.write(`${pkg.version}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const defaultStorePath = join(homedir(), ".ocas");
|
||||
const storePath =
|
||||
typeof flags.home === "string"
|
||||
@@ -121,7 +129,7 @@ async function out(data: unknown, store?: Store): Promise<void> {
|
||||
// varStore is intentionally omitted — inline render uses YAML fallback
|
||||
// only, custom templates require the full `ocas render` command.
|
||||
const output = renderDirect(envelope.type as Hash, envelope.value, s, null);
|
||||
process.stdout.write(output);
|
||||
process.stdout.write(`${output}\n`);
|
||||
return;
|
||||
}
|
||||
console.log(compact ? JSON.stringify(data) : JSON.stringify(data, null, 2));
|
||||
@@ -500,15 +508,7 @@ async function cmdRender(args: string[]): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
let store: Store;
|
||||
let varStore: VariableStore | undefined;
|
||||
if (isPipe) {
|
||||
store = await openStore();
|
||||
} else {
|
||||
const opened = await openStoreAndVarStore();
|
||||
store = opened.store;
|
||||
varStore = opened.varStore;
|
||||
}
|
||||
const { store, varStore } = await openStoreAndVarStore();
|
||||
|
||||
// Parse numeric options
|
||||
const resolution =
|
||||
@@ -571,35 +571,42 @@ async function cmdRender(args: string[]): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const output = renderDirect(
|
||||
envelope.type as Hash,
|
||||
envelope.value,
|
||||
store,
|
||||
{
|
||||
resolution,
|
||||
decay,
|
||||
epsilon,
|
||||
},
|
||||
);
|
||||
process.stdout.write(output);
|
||||
} else {
|
||||
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,
|
||||
epsilon,
|
||||
// If the envelope value is a hash string (e.g. from `put` output),
|
||||
// resolve it through renderAsync to apply templates and expand refs.
|
||||
// Otherwise, use renderDirect for inline rendering of the envelope value.
|
||||
if (typeof envelope.value === "string" && isHash(envelope.value)) {
|
||||
const output = await renderAsync(store, envelope.value as Hash, {
|
||||
...(resolution !== undefined && { resolution }),
|
||||
...(decay !== undefined && { decay }),
|
||||
...(epsilon !== undefined && { epsilon }),
|
||||
varStore,
|
||||
});
|
||||
// Output to stdout without JSON wrapping (raw output)
|
||||
process.stdout.write(output);
|
||||
} finally {
|
||||
varStore.close();
|
||||
process.stdout.write(`${output}\n`);
|
||||
} else {
|
||||
const output = renderDirect(
|
||||
envelope.type as Hash,
|
||||
envelope.value,
|
||||
store,
|
||||
{
|
||||
...(resolution !== undefined && { resolution }),
|
||||
...(decay !== undefined && { decay }),
|
||||
...(epsilon !== undefined && { epsilon }),
|
||||
},
|
||||
);
|
||||
process.stdout.write(`${output}\n`);
|
||||
}
|
||||
} else {
|
||||
const hash = resolveHash(input as string, varStore);
|
||||
const output = await renderAsync(store, hash, {
|
||||
...(resolution !== undefined && { resolution }),
|
||||
...(decay !== undefined && { decay }),
|
||||
...(epsilon !== undefined && { epsilon }),
|
||||
varStore,
|
||||
});
|
||||
// Output to stdout without JSON wrapping (raw output)
|
||||
process.stdout.write(`${output}\n`);
|
||||
}
|
||||
varStore.close();
|
||||
} catch (error) {
|
||||
if (error instanceof CasNodeNotFoundError) {
|
||||
die(`Error: Node not found: ${error.hash}`);
|
||||
@@ -760,9 +767,9 @@ async function cmdVarTag(args: string[]): Promise<void> {
|
||||
const { tags, labels, deleteNames } = parseTagsLabels(tagArgs);
|
||||
|
||||
const variable = varStore.tag(name, schema, {
|
||||
add: Object.keys(tags).length > 0 ? tags : undefined,
|
||||
addLabels: labels.length > 0 ? labels : undefined,
|
||||
delete: deleteNames.length > 0 ? deleteNames : undefined,
|
||||
...(Object.keys(tags).length > 0 && { add: tags }),
|
||||
...(labels.length > 0 && { addLabels: labels }),
|
||||
...(deleteNames.length > 0 && { delete: deleteNames }),
|
||||
});
|
||||
|
||||
await out(
|
||||
@@ -1033,7 +1040,7 @@ async function cmdTemplateDelete(args: string[]): Promise<void> {
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof VariableNotFoundError) {
|
||||
die(`Error: Template not found for schema: ${schemaHash}`);
|
||||
die(`Error: Template not found for schema: ${schemaInput}`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
@@ -1089,6 +1096,8 @@ async function cmdListSchema(_args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
const pkgPath = join(import.meta.dir, "..", "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
console.log(`\
|
||||
Usage: ocas [--home <path>] [--json] <command> [args]
|
||||
|
||||
@@ -1132,7 +1141,9 @@ 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 from stdin (put/hash: raw JSON payload; render: { type, value } envelope)`);
|
||||
--pipe, -p Read from stdin (put/hash: raw JSON payload; render: { type, value } envelope)
|
||||
|
||||
Agent: If you have not installed the ocas skill for this version (${pkg.version}), run \`ocas prompt setup\` and follow the instructions.`);
|
||||
}
|
||||
|
||||
// ---- Dispatch ----
|
||||
@@ -1241,6 +1252,33 @@ switch (cmd) {
|
||||
await cmdGc(rest);
|
||||
break;
|
||||
|
||||
case "prompt": {
|
||||
const [sub] = rest;
|
||||
switch (sub) {
|
||||
case "usage": {
|
||||
const content = readFileSync(
|
||||
join(import.meta.dir, "prompts", "usage.md"),
|
||||
"utf-8",
|
||||
);
|
||||
process.stdout.write(content);
|
||||
break;
|
||||
}
|
||||
case "setup": {
|
||||
const content = readFileSync(
|
||||
join(import.meta.dir, "prompts", "setup.md"),
|
||||
"utf-8",
|
||||
);
|
||||
process.stdout.write(content);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
die(
|
||||
`Unknown prompt subcommand: ${sub ?? "(none)"}. Available: usage, setup`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
die(`Unknown command: ${cmd}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# OCAS Skill Setup
|
||||
|
||||
You are being asked to install or update the OCAS (Object Content Addressable Store) skill
|
||||
so that you know how to use the `ocas` CLI.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Check if OCAS CLI is installed:**
|
||||
```bash
|
||||
ocas --help
|
||||
```
|
||||
If not installed: `bun add -g @ocas/cli`
|
||||
|
||||
2. **Clean up old versions of the skill** (if any exist):
|
||||
- Look for any existing OCAS/ocas skill files in your skill directories
|
||||
(e.g. `~/.hermes/skills/ocas/SKILL.md` or similar paths).
|
||||
- If a skill exists, read its `version` from the YAML frontmatter.
|
||||
- If the version matches the current CLI version (`ocas --version`), you're done — no update needed.
|
||||
- If the version is older, **delete the old skill file** before proceeding.
|
||||
|
||||
3. **Get the usage skill content:**
|
||||
```bash
|
||||
ocas prompt usage
|
||||
```
|
||||
|
||||
4. **Create a SKILL.md file** in your skill directory (e.g. `~/.hermes/skills/ocas/SKILL.md`).
|
||||
The output of `ocas prompt usage` is the skill body WITHOUT frontmatter.
|
||||
You must prepend the following YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: ocas
|
||||
description: "Use when storing, retrieving, or managing typed JSON data with OCAS (Object Content Addressable Store). Covers CLI usage, variable management, templates, rendering, and pipe composition."
|
||||
version: <VERSION>
|
||||
author: OCAS
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [cas, storage, cli, json, schema, content-addressing]
|
||||
---
|
||||
```
|
||||
|
||||
Replace `<VERSION>` with the installed CLI version (from `ocas --version`).
|
||||
|
||||
5. **Verify** the skill is loadable by your agent framework.
|
||||
|
||||
## Notes
|
||||
|
||||
- The skill content is bundled with the CLI and versioned with it — always use
|
||||
`ocas prompt usage` to get the content matching your installed version.
|
||||
- Do NOT hand-edit the skill body. If the CLI is updated, re-run `ocas prompt setup`
|
||||
and follow the steps again.
|
||||
- When upgrading, always delete the old skill first to avoid stale instructions.
|
||||
@@ -0,0 +1,197 @@
|
||||
# OCAS — Object Content Addressable Store
|
||||
|
||||
## Overview
|
||||
|
||||
OCAS is a self-describing content-addressable store for typed JSON data. Every node has a `type` field (hash of a JSON Schema) and a `payload`. Hashes are 13-character Crockford Base32 strings (XXH64 over deterministic CBOR).
|
||||
|
||||
All commands output `{ type, value }` JSON envelopes, making them composable via pipes.
|
||||
|
||||
**Install:** `bun add -g @ocas/cli`
|
||||
|
||||
**Packages:** `@ocas/core` (engine) · `@ocas/fs` (filesystem store) · `@ocas/cli` (CLI)
|
||||
|
||||
## When to Use
|
||||
|
||||
- Storing structured, schema-validated JSON data with content addressing
|
||||
- Building knowledge graphs or DAGs with typed nodes and `cas_ref` edges
|
||||
- Agent memory, config versioning, or any use case needing immutable data + mutable pointers
|
||||
- Don't use for: binary blobs, large files, or high-throughput streaming
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Register a schema (from stdin)
|
||||
echo '{
|
||||
"type": "object",
|
||||
"properties": { "title": { "type": "string" }, "done": { "type": "boolean" } },
|
||||
"required": ["title", "done"],
|
||||
"additionalProperties": false
|
||||
}' | ocas put @ocas/schema -p
|
||||
# → { "type": "...", "value": "<schema-hash>" }
|
||||
|
||||
# Name it
|
||||
ocas var set @todo/schema <schema-hash>
|
||||
|
||||
# Store data
|
||||
echo '{ "title": "Buy milk", "done": false }' | ocas put @todo/schema -p
|
||||
|
||||
# Retrieve + verify
|
||||
ocas get <hash>
|
||||
ocas verify <hash>
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Hashes
|
||||
13-char uppercase Crockford Base32 (e.g. `9S7JEYS3FKSDH`). Deterministic: same content → same hash.
|
||||
|
||||
### Envelope Format
|
||||
Every command outputs `{ type, value }`. `type` is the hash of the result schema. Pipe any envelope into `render -p` to render it human-readable.
|
||||
|
||||
### Variables
|
||||
Mutable pointers to immutable data (like git branches → commits). All names must follow `@scope/name` format:
|
||||
- `@myapp/config` ✅
|
||||
- `@ocas/schema` ✅ (builtin, read-only)
|
||||
- `config` ❌ (no scope)
|
||||
|
||||
### Templates
|
||||
LiquidJS templates bound to a schema. `render` uses the template for the node's type, falling back to YAML.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
### Store & Retrieve
|
||||
|
||||
```bash
|
||||
ocas put <type> <file> # store node → hash
|
||||
ocas put <type> -p # read payload from stdin
|
||||
ocas get <hash> # retrieve node
|
||||
ocas has <hash> # check existence
|
||||
ocas hash <type> <file> # compute hash without storing
|
||||
ocas verify <hash> # integrity + schema validation
|
||||
```
|
||||
|
||||
### Graph Traversal
|
||||
|
||||
```bash
|
||||
ocas refs <hash> # direct cas_ref edges
|
||||
ocas walk <hash> # recursive DAG traversal
|
||||
ocas walk <hash> --format tree # tree view
|
||||
```
|
||||
|
||||
### Listing & Querying
|
||||
|
||||
```bash
|
||||
ocas list --type <hash|name> # list nodes by type
|
||||
ocas list-schema # all schemas
|
||||
ocas list-meta # meta-schema hashes
|
||||
```
|
||||
|
||||
Sorting and pagination:
|
||||
|
||||
```bash
|
||||
ocas list --type @todo/schema --sort updated --desc --limit 20
|
||||
ocas list --type @todo/schema --offset 20 --limit 20 # page 2
|
||||
```
|
||||
|
||||
### Variables
|
||||
|
||||
```bash
|
||||
ocas var set @myapp/config <hash> # bind name → hash
|
||||
ocas var set @myapp/config <hash> --tag env:prod --tag pinned
|
||||
ocas var get @myapp/config # look up
|
||||
ocas var delete @myapp/config # remove
|
||||
ocas var list [prefix] # list (prefix filter)
|
||||
ocas var list @myapp/ --tag env:prod # filter by scope + tag
|
||||
ocas var history @myapp/config # last 10 values (LRU)
|
||||
ocas var tag @myapp/config --schema <h> status:active # add tag
|
||||
ocas var tag @myapp/config --schema <h> :status # remove tag
|
||||
```
|
||||
|
||||
**Naming rules:**
|
||||
- Format: `@scope/name` — `@[a-zA-Z][a-zA-Z0-9]*/segments`
|
||||
- `@ocas/*` reserved for builtins
|
||||
- Any command accepting a hash also accepts a variable name
|
||||
|
||||
### Templates & Rendering
|
||||
|
||||
```bash
|
||||
ocas template set <schema-hash> --inline "{{ payload.title }}"
|
||||
ocas template get <schema-hash>
|
||||
ocas template list
|
||||
ocas template delete <schema-hash>
|
||||
ocas render <hash> # render with template (or YAML fallback)
|
||||
ocas render --pipe/-p # render from piped envelope
|
||||
ocas get <hash> -r # inline render shorthand
|
||||
```
|
||||
|
||||
Render options: `--resolution N` (max depth), `--decay N` (depth decay), `--epsilon N` (cutoff).
|
||||
|
||||
### Garbage Collection
|
||||
|
||||
```bash
|
||||
ocas gc # collect unreachable nodes
|
||||
ocas gc | ocas render -p # human-readable stats
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--home <path>` | Store directory (default: `$OCAS_HOME` or `~/.ocas`) |
|
||||
| `--var-db <path>` | Variable database path |
|
||||
| `--json` | Compact JSON output |
|
||||
| `-p`, `--pipe` | Read from stdin |
|
||||
| `-r`, `--render` | Render output inline |
|
||||
| `--sort created\|updated` | Sort key (default: `created`) |
|
||||
| `--limit <n>` | Max results (default: 100) |
|
||||
| `--offset <n>` | Skip first N (default: 0) |
|
||||
| `--desc` | Sort descending |
|
||||
|
||||
## Pipe Composition Patterns
|
||||
|
||||
```bash
|
||||
# Store + render in one go
|
||||
echo '{"title":"test","done":false}' | ocas put @todo/schema -p | ocas render -p
|
||||
|
||||
# Or use -r shorthand
|
||||
ocas get <hash> -r
|
||||
|
||||
# List schemas, extract hashes with jq
|
||||
ocas list --type @ocas/schema | jq -r '.value[].hash'
|
||||
|
||||
# Render GC stats
|
||||
ocas gc | ocas render -p
|
||||
```
|
||||
|
||||
## Library Usage
|
||||
|
||||
```typescript
|
||||
import { bootstrap, createMemoryStore, putSchema } from "@ocas/core";
|
||||
|
||||
const store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
|
||||
const typeHash = await putSchema(store, {
|
||||
type: "object",
|
||||
properties: { message: { type: "string" } },
|
||||
required: ["message"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const hash = await store.put(typeHash, { message: "hello" });
|
||||
```
|
||||
|
||||
For filesystem persistence:
|
||||
|
||||
```typescript
|
||||
import { openStoreAndVarStore } from "@ocas/fs";
|
||||
const { store, varStore } = await openStoreAndVarStore("/path/to/store");
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Variable names without `@scope/`** — all names must be `@scope/name` format. `config` alone will be rejected.
|
||||
2. **Writing to `@ocas/*` namespace** — reserved for builtins, CLI will reject.
|
||||
3. **Forgetting `-p` for stdin** — `ocas put <type>` expects a file path; use `-p` to read from stdin.
|
||||
4. **Expecting `list` to return hashes** — `list` commands return `ListEntry[]` with `{ hash, created, updated }`, not bare hashes.
|
||||
5. **`workspace:*` in published packages** — only on `main` branch; release branches must have fixed versions.
|
||||
@@ -18,7 +18,7 @@ describe("ocas binary", () => {
|
||||
test("T2: no legacy bin entries (json-cas, ucas)", async () => {
|
||||
const pkg = await Bun.file(pkgPath).json();
|
||||
expect(pkg.bin["json-cas"]).toBeUndefined();
|
||||
expect(pkg.bin["ucas"]).toBeUndefined();
|
||||
expect(pkg.bin.ucas).toBeUndefined();
|
||||
expect(Object.keys(pkg.bin)).toEqual(["ocas"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { envValue, stripVolatile } from "./helpers";
|
||||
import { envValue } from "./helpers";
|
||||
|
||||
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
|
||||
|
||||
let tmpStore: string;
|
||||
let varDbPath: string;
|
||||
let typeHash: string;
|
||||
let nodeHash: string;
|
||||
let _nodeHash: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpStore = mkdtempSync(join(tmpdir(), "ocas-e2e-"));
|
||||
@@ -36,7 +36,7 @@ beforeAll(async () => {
|
||||
const nodeFile = join(tmpStore, "test-node.json");
|
||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||
const { stdout } = await runCli(["put", typeHash, nodeFile]);
|
||||
nodeHash = envValue(stdout) as string;
|
||||
_nodeHash = envValue(stdout) as string;
|
||||
|
||||
// Set up template for render tests
|
||||
const tmplFile = join(tmpStore, "render-template.liquid");
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Phase 5: Render", () => {
|
||||
return { stdout, stderr, exitCode };
|
||||
}
|
||||
|
||||
async function runCliE2eWithStdin(
|
||||
async function _runCliE2eWithStdin(
|
||||
args: string[],
|
||||
stdin: string,
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||
@@ -198,7 +198,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toBe("Hello Alice!");
|
||||
expect(output).toBe("Hello Alice!\n");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
@@ -321,7 +321,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toBe("Greetings Bob!");
|
||||
expect(output).toBe("Greetings Bob!\n");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ describe("Phase 2: Schema Validation", () => {
|
||||
let tmpStore: string;
|
||||
let varDbPath: string;
|
||||
let typeHash: string;
|
||||
let nodeHash: string;
|
||||
let _nodeHash: string;
|
||||
|
||||
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
|
||||
|
||||
@@ -602,7 +602,7 @@ describe("Phase 2: Schema Validation", () => {
|
||||
);
|
||||
await proc.exited;
|
||||
const stdout = (await new Response(proc.stdout).text()).trim();
|
||||
nodeHash = envValue(stdout) as string;
|
||||
_nodeHash = envValue(stdout) as string;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -183,7 +183,12 @@ describe("var set", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Update with hash2
|
||||
const { stdout, exitCode } = await runCli("var", "set", "@test/config", hash2);
|
||||
const { stdout, exitCode } = await runCli(
|
||||
"var",
|
||||
"set",
|
||||
"@test/config",
|
||||
hash2,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
@@ -203,7 +208,12 @@ describe("var set", () => {
|
||||
await runCli("var", "set", "@test/config", hash1);
|
||||
|
||||
// Create second variant with different schema
|
||||
const { stdout, exitCode } = await runCli("var", "set", "@test/config", hash2);
|
||||
const { stdout, exitCode } = await runCli(
|
||||
"var",
|
||||
"set",
|
||||
"@test/config",
|
||||
hash2,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
@@ -364,13 +374,25 @@ describe("var get", () => {
|
||||
await runCli("var", "set", "@test/config", hash2);
|
||||
|
||||
// Get first variant
|
||||
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
|
||||
const result1 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash1,
|
||||
);
|
||||
expect(result1.exitCode).toBe(0);
|
||||
const envelope1 = JSON.parse(result1.stdout);
|
||||
expect(envelope1.value.value).toBe(hash1);
|
||||
|
||||
// Get second variant
|
||||
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
|
||||
const result2 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash2,
|
||||
);
|
||||
expect(result2.exitCode).toBe(0);
|
||||
const envelope2 = JSON.parse(result2.stdout);
|
||||
expect(envelope2.value.value).toBe(hash2);
|
||||
@@ -399,10 +421,22 @@ describe("var delete", () => {
|
||||
expect(envelope.value.length).toBe(2);
|
||||
|
||||
// Verify both are deleted
|
||||
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
|
||||
const result1 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash1,
|
||||
);
|
||||
expect(result1.exitCode).toBe(1);
|
||||
|
||||
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
|
||||
const result2 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash2,
|
||||
);
|
||||
expect(result2.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
@@ -432,16 +466,32 @@ describe("var delete", () => {
|
||||
expect(envelope.value.schema).toBe(typeHash1);
|
||||
|
||||
// Verify first is deleted
|
||||
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
|
||||
const result1 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash1,
|
||||
);
|
||||
expect(result1.exitCode).toBe(1);
|
||||
|
||||
// Verify second still exists
|
||||
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
|
||||
const result2 = await runCli(
|
||||
"var",
|
||||
"get",
|
||||
"@test/config",
|
||||
"--schema",
|
||||
typeHash2,
|
||||
);
|
||||
expect(result2.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("return empty array when name not found", async () => {
|
||||
const { stdout, exitCode } = await runCli("var", "delete", "@test/nonexistent");
|
||||
const { stdout, exitCode } = await runCli(
|
||||
"var",
|
||||
"delete",
|
||||
"@test/nonexistent",
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
@@ -825,7 +875,16 @@ describe("var tag", () => {
|
||||
const hash = await createTestNode(store, typeHash, { test: "data" });
|
||||
|
||||
// Create variable with labels
|
||||
await runCli("var", "set", "@test/x", hash, "--tag", "stable", "--tag", "beta");
|
||||
await runCli(
|
||||
"var",
|
||||
"set",
|
||||
"@test/x",
|
||||
hash,
|
||||
"--tag",
|
||||
"stable",
|
||||
"--tag",
|
||||
"beta",
|
||||
);
|
||||
|
||||
// Delete label
|
||||
const { stdout, exitCode } = await runCli(
|
||||
@@ -912,7 +971,12 @@ describe("var tag", () => {
|
||||
});
|
||||
|
||||
test("error when --schema missing", async () => {
|
||||
const { stderr, exitCode } = await runCli("var", "tag", "@test/x", "env:prod");
|
||||
const { stderr, exitCode } = await runCli(
|
||||
"var",
|
||||
"tag",
|
||||
"@test/x",
|
||||
"env:prod",
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
"composite": false,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,47 +1,17 @@
|
||||
# @uncaged/json-cas
|
||||
# @ocas/core
|
||||
|
||||
## 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
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: add oneOf support to meta-schema validation
|
||||
- Fix TypeScript LSP errors: tsconfig `noEmit`, `exactOptionalPropertyTypes` conditional spread, `schemaHash` scope.
|
||||
|
||||
Added `oneOf` to `ALLOWED_SCHEMA_KEYS` and corresponding validation logic
|
||||
in `isValidSchema`. This enables workflow frontmatter schemas that use
|
||||
`oneOf` discriminated unions for multi-exit role definitions.
|
||||
## 0.1.1
|
||||
|
||||
## 0.3.0
|
||||
### Patch Changes
|
||||
|
||||
### Minor Changes
|
||||
- Internal improvements for v0.1.1 release.
|
||||
|
||||
- Disallow self-referencing nodes in put(). typeHash is now required (no null). Self-ref only via bootstrap().
|
||||
## 0.1.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add listByType(typeHash) to Store interface for O(1) type-based queries, with append-only fs index
|
||||
|
||||
## 0.1.3
|
||||
Initial release as `@ocas/core`. Content-addressable store engine with JSON Schema typed nodes, XXH64 hashing, variable store, render with resolution decay, and LiquidJS template support.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ocas/core",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -23,13 +23,13 @@ export {
|
||||
walk,
|
||||
} from "./schema.js";
|
||||
export { createMemoryStore } from "./store.js";
|
||||
export {
|
||||
type CasNode,
|
||||
type Hash,
|
||||
type ListEntry,
|
||||
type ListOptions,
|
||||
type ListSort,
|
||||
type Store,
|
||||
export type {
|
||||
CasNode,
|
||||
Hash,
|
||||
ListEntry,
|
||||
ListOptions,
|
||||
ListSort,
|
||||
Store,
|
||||
} from "./types.js";
|
||||
export type { Variable } from "./variable.js";
|
||||
export {
|
||||
|
||||
@@ -274,8 +274,12 @@ describe("VariableStore - set() Upsert Method", () => {
|
||||
expect(varA.value).not.toBe(varB.value);
|
||||
|
||||
// Verify both exist independently
|
||||
expect((varStore.get("@test/config", schemaA) as Variable).value).toBe(hashA);
|
||||
expect((varStore.get("@test/config", schemaB) as Variable).value).toBe(hashB);
|
||||
expect((varStore.get("@test/config", schemaA) as Variable).value).toBe(
|
||||
hashA,
|
||||
);
|
||||
expect((varStore.get("@test/config", schemaB) as Variable).value).toBe(
|
||||
hashB,
|
||||
);
|
||||
|
||||
varStore.close();
|
||||
});
|
||||
@@ -866,7 +870,9 @@ describe("VariableStore - Name Validation", () => {
|
||||
expect(() =>
|
||||
varStore.set("@test/deeply/nested/path/to/var", dataHash),
|
||||
).not.toThrow();
|
||||
expect(() => varStore.set("@test/uwf.thread.id_123", dataHash)).not.toThrow();
|
||||
expect(() =>
|
||||
varStore.set("@test/uwf.thread.id_123", dataHash),
|
||||
).not.toThrow();
|
||||
|
||||
varStore.close();
|
||||
});
|
||||
@@ -939,7 +945,9 @@ describe("VariableStore - Name Validation", () => {
|
||||
expect(() => varStore.set("a//b", dataHash)).toThrow(
|
||||
InvalidVariableNameError,
|
||||
);
|
||||
expect(() => varStore.set("a//b", dataHash)).toThrow(/must follow @scope\/name|empty segment/i);
|
||||
expect(() => varStore.set("a//b", dataHash)).toThrow(
|
||||
/must follow @scope\/name|empty segment/i,
|
||||
);
|
||||
|
||||
// Triple slash
|
||||
expect(() => varStore.set("a///b", dataHash)).toThrow(
|
||||
@@ -962,13 +970,17 @@ describe("VariableStore - Name Validation", () => {
|
||||
expect(() => varStore.set("/abc", dataHash)).toThrow(
|
||||
InvalidVariableNameError,
|
||||
);
|
||||
expect(() => varStore.set("/abc", dataHash)).toThrow(/must follow @scope\/name|leading slash/i);
|
||||
expect(() => varStore.set("/abc", dataHash)).toThrow(
|
||||
/must follow @scope\/name|leading slash/i,
|
||||
);
|
||||
|
||||
// Trailing slash
|
||||
expect(() => varStore.set("abc/", dataHash)).toThrow(
|
||||
InvalidVariableNameError,
|
||||
);
|
||||
expect(() => varStore.set("abc/", dataHash)).toThrow(/must follow @scope\/name|trailing slash/i);
|
||||
expect(() => varStore.set("abc/", dataHash)).toThrow(
|
||||
/must follow @scope\/name|trailing slash/i,
|
||||
);
|
||||
|
||||
// Both
|
||||
expect(() => varStore.set("/abc/", dataHash)).toThrow(
|
||||
@@ -1095,7 +1107,9 @@ describe("VariableStore - validateName() Error Messages", () => {
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(InvalidVariableNameError);
|
||||
const error = e as InvalidVariableNameError;
|
||||
expect(error.reason).toMatch(/empty segment|consecutive|leading|start|begins/i);
|
||||
expect(error.reason).toMatch(
|
||||
/empty segment|consecutive|leading|start|begins/i,
|
||||
);
|
||||
expect(error.reason).not.toMatch(/trailing|end/i);
|
||||
}
|
||||
|
||||
@@ -1124,8 +1138,12 @@ describe("VariableStore - validateName() Error Messages", () => {
|
||||
expect(() => varStore.set("@test/app.config", dataHash)).not.toThrow();
|
||||
expect(() => varStore.set("@test/my_variable", dataHash)).not.toThrow();
|
||||
expect(() => varStore.set("@test/test-name", dataHash)).not.toThrow();
|
||||
expect(() => varStore.set("@test/path/to/config.json", dataHash)).not.toThrow();
|
||||
expect(() => varStore.set("@test/v1.2.3-alpha_001", dataHash)).not.toThrow();
|
||||
expect(() =>
|
||||
varStore.set("@test/path/to/config.json", dataHash),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
varStore.set("@test/v1.2.3-alpha_001", dataHash),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1335,7 +1353,10 @@ describe("VariableStore - List Operation", () => {
|
||||
const vars = varStore.list();
|
||||
|
||||
expect(vars.length).toBe(2);
|
||||
expect(vars.map((v) => v.name).sort()).toEqual(["@test/var1", "@test/var2"]);
|
||||
expect(vars.map((v) => v.name).sort()).toEqual([
|
||||
"@test/var1",
|
||||
"@test/var2",
|
||||
]);
|
||||
|
||||
varStore.close();
|
||||
});
|
||||
@@ -1436,7 +1457,10 @@ describe("VariableStore - list() with exactName", () => {
|
||||
varStore.set("@test/config", valueB);
|
||||
|
||||
// When: Filter by both exactName and schema
|
||||
const results = varStore.list({ exactName: "@test/config", schema: schemaA });
|
||||
const results = varStore.list({
|
||||
exactName: "@test/config",
|
||||
schema: schemaA,
|
||||
});
|
||||
|
||||
// Then: Returns only schemaA variant
|
||||
expect(results.length).toBe(1);
|
||||
|
||||
@@ -1,70 +1,19 @@
|
||||
# @uncaged/json-cas-fs
|
||||
# @ocas/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`
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.6.0
|
||||
- Updated dependencies:
|
||||
- @ocas/core@0.1.2
|
||||
|
||||
## 0.5.3
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: add oneOf support to meta-schema validation
|
||||
- Updated dependencies:
|
||||
- @ocas/core@0.1.1
|
||||
|
||||
Added `oneOf` to `ALLOWED_SCHEMA_KEYS` and corresponding validation logic
|
||||
in `isValidSchema`. This enables workflow frontmatter schemas that use
|
||||
`oneOf` discriminated unions for multi-exit role definitions.
|
||||
## 0.1.0
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.5.3
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Disallow self-referencing nodes in put(). typeHash is now required (no null). Self-ref only via bootstrap().
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.3.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add listByType(typeHash) to Store interface for O(1) type-based queries, with append-only fs index
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.2.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.1.3
|
||||
Initial release as `@ocas/fs`. Filesystem-backed CAS store with SQLite indexing and auto-bootstrap.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ocas/fs",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cborg": "^4.2.3",
|
||||
"@ocas/core": "0.1.0"
|
||||
"@ocas/core": "0.1.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -71,6 +71,23 @@ info "New version: $NEW_VERSION"
|
||||
git branch -m "release/next" "release/$NEW_VERSION"
|
||||
info "Release branch: release/$NEW_VERSION"
|
||||
|
||||
# --- Clean up changesets on main ---
|
||||
|
||||
info "Removing consumed changesets from main..."
|
||||
git stash --include-untracked
|
||||
git checkout main
|
||||
# Delete the changeset files that were consumed
|
||||
for cs in $CHANGESETS; do
|
||||
rm -f "$cs"
|
||||
done
|
||||
git add -A
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
git commit -m "chore: remove changesets consumed by release/$NEW_VERSION"
|
||||
git push origin main
|
||||
fi
|
||||
git checkout "release/$NEW_VERSION"
|
||||
git stash pop || true
|
||||
|
||||
# --- Validate ---
|
||||
|
||||
echo ""
|
||||
@@ -101,5 +118,6 @@ echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Review changes: git diff main...HEAD"
|
||||
echo " 2. Fix issues if needed, commit to this branch"
|
||||
echo " 3. When ready: ./scripts/publish.sh"
|
||||
echo " 3. Prerelease: ./scripts/publish.sh --rc"
|
||||
echo " 4. Final release: ./scripts/publish.sh"
|
||||
echo ""
|
||||
|
||||
+119
-38
@@ -4,7 +4,8 @@ set -euo pipefail
|
||||
# OCAS Publish
|
||||
# Builds, publishes to npm, tags, and pushes.
|
||||
#
|
||||
# Usage: ./scripts/publish.sh
|
||||
# Usage: ./scripts/publish.sh # publish final release
|
||||
# ./scripts/publish.sh --rc # publish prerelease (rc tag)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - On a release/* branch (created by prepare-release.sh)
|
||||
@@ -21,6 +22,13 @@ info() { echo -e "${BOLD}${GREEN}✓${NC} $1"; }
|
||||
warn() { echo -e "${BOLD}${YELLOW}⚠${NC} $1"; }
|
||||
error() { echo -e "${BOLD}${RED}✗${NC} $1"; exit 1; }
|
||||
|
||||
# --- Parse flags ---
|
||||
|
||||
RC_MODE=false
|
||||
if [[ "${1:-}" == "--rc" ]]; then
|
||||
RC_MODE=true
|
||||
fi
|
||||
|
||||
# --- Pre-flight checks ---
|
||||
|
||||
echo -e "\n${BOLD}OCAS Publish${NC}\n"
|
||||
@@ -32,14 +40,31 @@ BRANCH=$(git branch --show-current)
|
||||
# Clean working tree
|
||||
[[ -z "$(git status --porcelain)" ]] || error "Working tree is not clean. Commit changes first."
|
||||
|
||||
# Extract version
|
||||
VERSION=$(python3 -c "import json; print(json.load(open('packages/core/package.json'))['version'])")
|
||||
info "Publishing version: $VERSION"
|
||||
# Extract base version from package.json
|
||||
BASE_VERSION=$(python3 -c "import json; print(json.load(open('packages/core/package.json'))['version'])")
|
||||
|
||||
# No pending changesets (should have been consumed by prepare-release.sh)
|
||||
CHANGESETS=$(ls .changeset/*.md 2>/dev/null | grep -v README.md || true)
|
||||
if [[ -n "$CHANGESETS" ]]; then
|
||||
error "Pending changesets found. Run prepare-release.sh first."
|
||||
# Determine publish version and npm tag
|
||||
if [[ "$RC_MODE" == "true" ]]; then
|
||||
# Find next rc number
|
||||
EXISTING_RC=$(npm view "@ocas/core" versions --json 2>/dev/null | python3 -c "
|
||||
import json, sys, re
|
||||
versions = json.load(sys.stdin)
|
||||
if not isinstance(versions, list): versions = [versions]
|
||||
rc_nums = [int(m.group(1)) for v in versions if (m := re.match(r'^${BASE_VERSION}-rc\.(\d+)$', v))]
|
||||
print(max(rc_nums) + 1 if rc_nums else 1)
|
||||
" 2>/dev/null || echo 1)
|
||||
VERSION="${BASE_VERSION}-rc.${EXISTING_RC}"
|
||||
NPM_TAG="rc"
|
||||
info "Prerelease mode: $VERSION (npm tag: rc)"
|
||||
else
|
||||
VERSION="$BASE_VERSION"
|
||||
NPM_TAG="latest"
|
||||
|
||||
# No pending changesets (should have been consumed by prepare-release.sh)
|
||||
CHANGESETS=$(ls .changeset/*.md 2>/dev/null | grep -v README.md || true)
|
||||
if [[ -n "$CHANGESETS" ]]; then
|
||||
error "Pending changesets found. Run prepare-release.sh first."
|
||||
fi
|
||||
fi
|
||||
|
||||
# npm auth check
|
||||
@@ -47,6 +72,24 @@ npm whoami &>/dev/null || error "Not authenticated with npm. Run 'npm login' fir
|
||||
NPM_USER=$(npm whoami)
|
||||
info "npm user: $NPM_USER"
|
||||
|
||||
# --- Set version in all packages ---
|
||||
|
||||
info "Setting version: $VERSION"
|
||||
for pkg in core fs cli; do
|
||||
python3 -c "
|
||||
import json
|
||||
path = 'packages/$pkg/package.json'
|
||||
data = json.load(open(path))
|
||||
data['version'] = '$VERSION'
|
||||
# Fix workspace:* to actual version for publishing
|
||||
for dep in list(data.get('dependencies', {})):
|
||||
if data['dependencies'][dep] == 'workspace:*':
|
||||
data['dependencies'][dep] = '$VERSION'
|
||||
json.dump(data, open(path, 'w'), indent=2)
|
||||
open(path, 'a').write('\n')
|
||||
"
|
||||
done
|
||||
|
||||
# --- Final validation ---
|
||||
|
||||
info "Running final validation..."
|
||||
@@ -57,53 +100,91 @@ bun run build
|
||||
echo " → bun test"
|
||||
bun test || error "Tests failed! Fix before publishing."
|
||||
|
||||
# --- Confirm ---
|
||||
# --- Publish (order matters: core → fs → cli) ---
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Will publish:${NC}"
|
||||
for pkg in core fs cli; do
|
||||
PKG_NAME=$(python3 -c "import json; print(json.load(open('packages/$pkg/package.json'))['name'])")
|
||||
echo " $PKG_NAME@$VERSION"
|
||||
echo " $PKG_NAME@$VERSION (tag: $NPM_TAG)"
|
||||
done
|
||||
# --- Publish (order matters: core → fs → cli) ---
|
||||
|
||||
for pkg in core fs cli; do
|
||||
PKG_NAME=$(python3 -c "import json; print(json.load(open('packages/$pkg/package.json'))['name'])")
|
||||
echo ""
|
||||
info "Publishing $PKG_NAME@$VERSION..."
|
||||
(cd "packages/$pkg" && bun publish --access public)
|
||||
(cd "packages/$pkg" && bun publish --access public --tag "$NPM_TAG")
|
||||
info "$PKG_NAME@$VERSION published ✓"
|
||||
done
|
||||
|
||||
# --- Tag and push ---
|
||||
# --- Commit version changes ---
|
||||
|
||||
echo ""
|
||||
TAG="v$VERSION"
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "$BRANCH"
|
||||
git push origin "$TAG"
|
||||
info "Tag $TAG pushed"
|
||||
git add -A
|
||||
if [[ -n "$(git diff --cached --name-only)" ]]; then
|
||||
git commit -m "chore(release): set version $VERSION"
|
||||
fi
|
||||
|
||||
# --- Merge back to main ---
|
||||
# --- For final release: tag, push, merge back ---
|
||||
|
||||
echo ""
|
||||
info "Merging release into main..."
|
||||
git checkout main
|
||||
git merge "$BRANCH" --no-ff -m "chore: merge release $TAG"
|
||||
git push origin main
|
||||
info "Merged to main"
|
||||
if [[ "$RC_MODE" == "false" ]]; then
|
||||
TAG="v$VERSION"
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "$BRANCH" --tags
|
||||
git push github "$BRANCH" --tags 2>/dev/null || true
|
||||
info "Tag $TAG pushed"
|
||||
|
||||
# Clean up release branch
|
||||
git branch -d "$BRANCH"
|
||||
git push origin --delete "$BRANCH" 2>/dev/null || true
|
||||
info "Release branch cleaned up"
|
||||
# Merge back to main
|
||||
echo ""
|
||||
info "Merging release into main..."
|
||||
git checkout main
|
||||
git merge "$BRANCH" --no-ff -m "chore: merge release $TAG"
|
||||
|
||||
echo ""
|
||||
info "Release $TAG complete! 🎉"
|
||||
echo ""
|
||||
echo " Published:"
|
||||
for pkg in core fs cli; do
|
||||
PKG_NAME=$(python3 -c "import json; print(json.load(open('packages/$pkg/package.json'))['name'])")
|
||||
echo " https://www.npmjs.com/package/$PKG_NAME"
|
||||
done
|
||||
# Restore workspace:* on main
|
||||
for pkg in fs cli; do
|
||||
python3 -c "
|
||||
import json
|
||||
path = 'packages/$pkg/package.json'
|
||||
data = json.load(open(path))
|
||||
for dep in list(data.get('dependencies', {})):
|
||||
if dep.startswith('@ocas/'):
|
||||
data['dependencies'][dep] = 'workspace:*'
|
||||
json.dump(data, open(path, 'w'), indent=2)
|
||||
open(path, 'a').write('\n')
|
||||
"
|
||||
done
|
||||
git add -A
|
||||
git commit -m "chore: restore workspace:* deps on main" || true
|
||||
|
||||
git push origin main
|
||||
git push github main 2>/dev/null || true
|
||||
info "Merged to main"
|
||||
|
||||
# Clean up release branch
|
||||
git branch -d "$BRANCH"
|
||||
git push origin --delete "$BRANCH" 2>/dev/null || true
|
||||
git push github --delete "$BRANCH" 2>/dev/null || true
|
||||
info "Release branch cleaned up"
|
||||
|
||||
echo ""
|
||||
info "Release $TAG complete! 🎉"
|
||||
echo ""
|
||||
echo " Published:"
|
||||
for pkg in core fs cli; do
|
||||
PKG_NAME=$(python3 -c "import json; print(json.load(open('packages/$pkg/package.json'))['name'])")
|
||||
echo " https://www.npmjs.com/package/$PKG_NAME"
|
||||
done
|
||||
else
|
||||
# RC: just push the branch
|
||||
git push origin "$BRANCH"
|
||||
git push github "$BRANCH" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
info "Prerelease $VERSION published! 🏷️"
|
||||
echo ""
|
||||
echo " Install for testing:"
|
||||
echo " bun add -g @ocas/cli@rc"
|
||||
echo ""
|
||||
echo " When verified, run final release:"
|
||||
echo " ./scripts/publish.sh"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user