49 Commits

Author SHA1 Message Date
xiaoju 1bd4edbdd1 fix: disable pnpm minimumReleaseAge in CI
CI / check (pull_request) Failing after 1m21s
pnpm 11 defaults minimumReleaseAge to 1440 min. Set to 0 in
pnpm-workspace.yaml for our own packages.
2026-06-04 10:13:46 +00:00
xiaoju 49f096b18f fix: use corepack for pnpm in CI
CI / check (pull_request) Failing after 32s
Drop pnpm/action-setup (requires explicit version), use corepack enable instead.
Also align Node to 22 (consistent with uwf).

Closes #76
2026-06-04 10:07:28 +00:00
xiaoju 6b9ebd1796 chore: add Gitea CI workflow (build + lint + test)
CI / check (pull_request) Failing after 1m18s
小橘 🍊(NEKO Team)
2026-06-04 09:36:51 +00:00
xiaomo b3879c583a Merge pull request 'chore: use proman test instead of vitest run' (#75) from chore/proman-test-script into main
chore: use proman test instead of vitest run (#75)
2026-06-04 01:57:23 +00:00
xingyue 01d4f0fa14 chore: use proman test instead of vitest run 2026-06-04 09:51:30 +08:00
xiaomo d79d0227fa Merge pull request 'fix: resolve prompt files from package root instead of dist' (#74) from fix/prompt-path into main
fix: resolve prompt files from package root, suppress ExperimentalWarning in tests (#74)
2026-06-04 00:38:14 +00:00
tuanzi e4e4ce0f73 fix: suppress SQLite ExperimentalWarning in edge-cases and schema-validation tests
The local runCli helpers in edge-cases.test.ts (Phase 3/4/7) and raw
execFileSync calls in schema-validation.test.ts (tests 2.1, 2.3) were
missing NODE_NO_WARNINGS=1, causing ExperimentalWarning to leak into
stderr snapshots. Added env override to match what helpers.runCli
already does.
2026-06-04 00:36:38 +00:00
tuanzi 13b12ef50c fix: resolve prompt files from package root instead of dist
prompts/*.md files are not copied to dist/ during tsc build,
causing `ocas prompt setup/usage` to fail with ENOENT.

- Change join(__dirname, "prompts") → join(__dirname, "..", "prompts")
- Add prompts/ to package.json "files" for npm publishing
- Update snapshots (Node.js SQLite ExperimentalWarning + version string)
2026-06-04 00:11:28 +00:00
xiaomo a9d43abf28 release: @ocas/cli@0.3.1 2026-06-04 00:01:29 +00:00
xiaomo 6f054f6447 fix(cli): update prompts — bun→pnpm, remove stale --var-db flag 2026-06-03 23:49:11 +00:00
xiaomo 5e9b266ebd docs: add pkg descriptions/keywords/engines, fix core README
Fixes #70, #71, #72, #73
2026-06-03 23:23:48 +00:00
xiaomo c4d9205eb2 docs: update all documentation for node:sqlite, pnpm, proman workflow
- README: bun → pnpm, update API examples, add Node >=22.5.0 requirement
- CLAUDE.md: replace 3-phase release process with proman bump/publish
- Package READMEs: fix package names, update storage/API descriptions
- Cards: update store.md (sqlite), cli.md (db filename, remove --var-db)
- docs/sync-readme.md: update to proman workflow
2026-06-03 23:21:30 +00:00
xiaoju 9d17be8b7b release: v0.3.0 2026-06-03 23:06:00 +00:00
xiaomo 7e9bd26fec fix: suppress ExperimentalWarning in tests via NODE_NO_WARNINGS env
- Use NODE_NO_WARNINGS=1 in execFileSync env instead of --no-warnings flag
- Remove overly broad process.removeAllListeners('warning') from CLI entry
- Add engines field requiring Node >=22.5.0 (node:sqlite availability)
- Update proman to 0.4.2
2026-06-03 23:02:43 +00:00
xiaomo 3168bf55c3 chore: add changeset for node:sqlite migration 2026-06-03 22:30:15 +00:00
xiaomo 00a536631a refactor: migrate from better-sqlite3 to node:sqlite
- Replace better-sqlite3 with built-in node:sqlite (DatabaseSync)
- Add transaction() helper for manual BEGIN/COMMIT/ROLLBACK
- Suppress ExperimentalWarning in CLI tests with --no-warnings
- Remove better-sqlite3 from dependencies and onlyBuiltDependencies
- No more native addon compilation issues across Node versions
2026-06-03 22:11:44 +00:00
xiaomo f8103e20ce chore: bump @shazhou/proman to 0.4.0, remove link override 2026-06-03 16:30:24 +00:00
xiaomo 5c567dc455 Revert "Merge pull request 'chore: remove redundant vite/vitest devDeps' (#69) from chore/remove-redundant-devdeps into main"
This reverts commit 08a2bddcf0, reversing
changes made to 36ebf42f2f.
2026-06-03 15:15:38 +00:00
xiaomo 08a2bddcf0 Merge pull request 'chore: remove redundant vite/vitest devDeps' (#69) from chore/remove-redundant-devdeps into main 2026-06-03 14:28:35 +00:00
xiaomo 6fc3b9030b chore: remove redundant vite/vitest devDeps (proman bundles them) 2026-06-03 14:25:35 +00:00
xiaoju 36ebf42f2f chore: bump 0.2.2, fix lint, biome format
- Bump all packages to 0.2.2
- Fix noNonNullAssertion in cli/index.ts
- Fix unused imports in fs/sqlite-store.ts
- Biome 2.4.16 migration + format all files
- Update snapshots for version change
2026-06-03 11:16:16 +00:00
xiaoju f286df91f0 chore: restore workspace:* 2026-06-03 09:43:57 +00:00
xiaoju 8971206a3e Merge release/0.2.1 into main 2026-06-03 09:43:37 +00:00
xiaoju 807c0942e8 release: v0.2.1
- Clean dist: no stale variable-store.js (bun:sqlite reference removed)
- Fix vitest rejects pattern in liquid-render tests
- proman.yaml runtime: bun → node
- Upgrade @shazhou/proman to 0.2.2
2026-06-03 09:43:27 +00:00
xiaoju 5c0670e69b release: prepare v0.2.1 2026-06-03 09:40:07 +00:00
xiaoju c90efda4dd chore: upgrade @shazhou/proman to 0.2.2 2026-06-03 09:39:59 +00:00
xiaoju 2d4f76330c fix: vitest rejects pattern + runtime config
- Fix 4 liquid-render tests: expect(async()=>{}).rejects → expect(promise).rejects
- proman.yaml: runtime bun → node (matches actual stack)
- package.json: test script uses vitest directly (avoid proman recursion)
- Add changeset for 0.2.1 patch (clean dist)
2026-06-03 09:30:23 +00:00
xiaomo 768ae20a24 Merge pull request 'feat: migrate var/tag store from JSONL to better-sqlite3' (#68) from feat/60-sqlite-var-tag into main
feat: migrate var/tag store from JSONL to better-sqlite3 (#68)
2026-06-03 08:40:45 +00:00
xiaoju 72ba313d12 fix: address review — transactions, LIKE escape, post-filter pagination
Critical fixes:
1. LIKE ESCAPE '\' clause added for namePrefix queries
2. list() limit/offset now applied AFTER tag/label post-filter
3. tag()/untag() wrapped in db.transaction()

Warning fixes:
4. set()/update() wrapped in db.transaction() (txnSetVar)
5. listByTag sort/limit/offset pushed to SQL
6. close() idempotent (double-close guard)
7. JSONL → SQLite migration on first open (vars + tags)
8. History truncation simplified (NOT IN subquery)
9. Removed unnecessary pre-fetch in upsert (ON CONFLICT handles it)

Nits:
10. Database.Database type instead of InstanceType
11. row.name instead of row["name"]
12. DESC index on var_history position
13. key+value composite index on tags
2026-06-03 08:38:44 +00:00
xiaoju 1fe6035be5 feat: migrate var/tag store from JSONL to better-sqlite3
- New sqlite-store.ts: VarStore + TagStore backed by SQLite (WAL mode)
- Tables: vars (composite PK name+schema), var_history (with MAX_HISTORY truncation), tags (composite PK target+key)
- Indexed queries for name, created, updated, tag key
- Tags/labels stored as JSON columns with post-filter
- Removed var-store.ts (JSONL append-log implementation)
- Updated tests: JSONL assertions → SQLite db file checks

Closes #60
2026-06-03 08:29:22 +00:00
xiaoju e3f7ec1a11 Merge pull request 'chore: 去掉 Bun,切换到 pnpm + 纯 Node runtime' (#67) from chore/66-remove-bun into main 2026-06-03 07:55:03 +00:00
xiaoju 9ac08e5893 chore: 去掉 Bun,切换到 pnpm + 纯 Node runtime
- bun.lock → pnpm-lock.yaml + pnpm-workspace.yaml
- 删除 sqlite-adapter.ts(Bun/Node 双 runtime 兼容层,未被使用)
- package.json 删除 workspaces 字段,加 pnpm.onlyBuiltDependencies
- 加 vite 8 显式依赖(vitest 4.x peer dep)
- CLAUDE.md 全面更新:Runtime/Commands/Release 流程
- .gitignore 加 bun.lock

36/36 files pass, 617/617 tests pass

Fixes #66
2026-06-03 07:43:09 +00:00
xiaoju fe56634160 fix: clean build 兼容性修复
- 根 package.json 加 @types/node(clean install 后 tsc 需要)
- CLI tests entrypoint 从 src/index.ts 改为 dist/index.js
- 局部 runCli 支持 rest 和 array 两种调用模式(rawArgs.flat())
- 所有剩余 tsx 引用改为 node

36/36 files pass, 617/617 tests pass

Fixes #64
2026-06-03 06:32:39 +00:00
xiaoju dd75c1f39d Merge pull request 'fix: 修复 vitest 迁移后测试失败' (#65) from fix/64-test-failures into main 2026-06-03 06:16:56 +00:00
xiaoju 32b520b2a4 fix: 修复 vitest 迁移后测试失败
- async 函数断言改用 await expect().rejects.toThrow()
- 所有局部 runCli 从 tsx 改为 node
- rest params (...args) 改为 array params (args)
- 更新 snapshots

36/36 files pass, 617/617 tests pass

Fixes #64
2026-06-03 06:16:33 +00:00
xiaoju 05f3246e5b Merge pull request 'chore: 测试框架从 bun:test 迁移到 vitest' (#63) from chore/62-vitest into main 2026-06-03 04:16:29 +00:00
xiaoju 736d7e7374 chore: 测试框架从 bun:test 迁移到 vitest
- 36 个 test 文件 bun:test → vitest
- Bun.spawn() → execFileSync('tsx', ...)
- Bun.file() → readFileSync
- import.meta.dir → import.meta.dirname (tests) / __dirname (CLI source)
- 删除 bun-types devDep
- 添加 vitest + tsx devDep
- CLI shebang bun → node
- 30/36 test files pass, 558/617 tests pass

Refs #62
2026-06-03 04:11:10 +00:00
xiaomo 3cfcf6db89 Merge pull request 'feat: cli 包完整 build 支持(tsc emit + Node 兼容)' (#61) from feat/58-cli-build into main 2026-06-03 03:28:31 +00:00
xiaoju 5f562cbc5a feat: cli 包完整 build 支持(tsc emit + Node 兼容)
- tsconfig: 启用 emit (rootDir/outDir/composite), 添加 project references
- shebang: bun → node
- bin: src/index.ts → dist/index.js
- import.meta.dir → import.meta.dirname (去掉 Bun 专有 API)
- prompts 目录移到包根, src/dist 路径统一
- 修复 exactOptionalPropertyTypes 类型错误

Fixes #58
2026-06-03 02:29:01 +00:00
xiaoju 234bd0d611 chore: remove legacy release scripts (replaced by proman)
小橘 🍊
2026-06-03 00:37:15 +00:00
xiaoju ff4c4f3eff chore: remove per-package scripts (test/prepublishOnly)
All dev commands handled by proman at root level.
workspace:* safety handled by proman release flow.

小橘 🍊
2026-06-03 00:36:04 +00:00
xiaoju 39e4e4faca chore: remove changeset deps, release now handled by proman
小橘 🍊
2026-06-03 00:25:04 +00:00
xiaoju 799ef0af92 chore: consolidate dev deps via @shazhou/proman
Remove direct biome and typescript devDependencies.
All dev commands (build/test/check/format) now go through proman.

小橘 🍊
2026-06-03 00:10:14 +00:00
xiaoju 845ecb635e chore: add per-package build scripts for proman compatibility
小橘 🍊
2026-06-02 16:30:27 +00:00
xiaoju aa0584dea7 chore: switch proman config from TS to YAML 2026-06-02 15:47:25 +00:00
xiaoju 12b61b4625 fix: set correct version in fs/cli package.json (was workspace:*) 2026-06-02 14:40:41 +00:00
xiaoju 22dec2ae7a chore: add proman.config.ts 2026-06-02 14:40:01 +00:00
xiaoju 4d6d090bb9 chore: restore workspace:* on main 2026-06-02 12:53:58 +00:00
xiaoju 6d89c60401 Merge release/0.2.0 into main 2026-06-02 12:53:48 +00:00
75 changed files with 3913 additions and 1812 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ The `ocas` CLI is the primary interface for interacting with an OCAS [[Store]].
| 2 | `OCAS_HOME` env var | `export OCAS_HOME=/data/ocas` |
| 3 | Default | `~/.ocas` |
The variable database lives at `<home>/variables.db` by default, overridable with `--var-db <path>`.
The SQLite database lives at `<home>/_store.db`.
## Commands
+1 -1
View File
@@ -53,7 +53,7 @@ In-memory `Map<Hash, CasNode>`. Used in tests and for ephemeral computation (e.g
### FsStore (`@ocas/fs`)
Filesystem-backed store. Nodes are serialized as CBOR files under a content-addressed directory tree. Created via `openStore(path)`, which:
Filesystem-backed store. CAS nodes are stored as CBOR files; variables and tags use SQLite (`node:sqlite`). Created via `openStore(path)`, which:
1. Creates the directory if it doesn't exist
2. Runs [[Bootstrap]] automatically
+28
View File
@@ -0,0 +1,28 @@
name: CI
on:
push:
branches: ['*']
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: corepack enable && pnpm install
- name: Build
run: pnpm run build
- name: Lint
run: pnpm run check
- name: Test
run: pnpm run test
+1
View File
@@ -3,3 +3,4 @@ dist/
*.d.ts.map
*.tsbuildinfo
.worktrees/
bun.lock
+21 -52
View File
@@ -14,20 +14,21 @@ Monorepo with 3 packages under `packages/`:
## Tech Stack
- **Runtime:** Bun
- **Runtime:** Node.js
- **Language:** TypeScript (strict mode, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`)
- **Build:** `tsc --build` (composite project references)
- **Test:** `bun test`
- **Build:** `tsc` (composite project references, sequential: core → fs → cli)
- **Test:** Vitest (`npx vitest run`)
- **Package Manager:** pnpm (workspace)
- **Lint/Format:** Biome (`biome check .` / `biome format --write .`)
- **Publish:** Changesets + `bun publish` → npmjs (`@ocas/*`)
- **Publish:** @shazhou/proman (`proman bump` + `proman publish`)
## Commands
```bash
bun test # Run all tests
bun run build # Build all packages
bun run check # Biome lint
bun run format # Biome format (auto-fix)
pnpm run test # Run all tests (vitest)
pnpm run build # Build all packages (tsc via proman)
pnpm run check # Biome lint
pnpm run format # Biome format (auto-fix)
```
## Code Conventions
@@ -72,7 +73,7 @@ bun run format # Biome format (auto-fix)
- **`bootstrap(store)`** synchronously writes builtin name → hash bindings into the unified store; called automatically by `openStore()`.
- **`resolveHash(input, store)`** is the unified hash/name resolver in the CLI. If `input` matches the 13-char hash format it is returned as-is; otherwise `store.var` is queried by exact name. This means every CLI command that accepts a hash argument also accepts a variable name (schema names, user vars, etc.).
- **Variable naming**: all names must follow `@scope/name` format (`@[a-zA-Z][a-zA-Z0-9]*/segments`). `@ocas/*` is reserved for builtins. The `@` prefix ensures names are visually distinct from hashes.
- **`openStore()`** returns a unified `Store` with `cas`, `var`, and `tag` sub-stores, and bootstraps automatically. `@ocas/core` has zero `bun:sqlite` dependency.
- **`openStore()`** returns a unified `Store` with `cas`, `var`, and `tag` sub-stores, and bootstraps automatically. `@ocas/core` has zero SQLite dependency.
### Internal Dependencies
@@ -91,16 +92,13 @@ This is resolved to real version numbers only during publishing (see below).
## Before Submitting
1. `bun test` — all tests pass
2. `bun run check` — no lint errors
3. `bun run build` — builds cleanly
1. `pnpm run test` — all tests pass
2. `pnpm run check` — no lint errors
3. `pnpm run build` — builds cleanly
## Release Process
Releases use a **release branch** workflow with three phases: prepare → candidate → finalize.
`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.
Uses `@shazhou/proman` for releases. No release branches needed.
### Adding a Changeset
@@ -109,7 +107,7 @@ Add changesets alongside feature PRs on `main`:
```markdown
<!-- .changeset/my-change.md -->
---
"@ocas/cli": patch
"@ocas/fs": minor
---
Description of the change
@@ -118,44 +116,15 @@ Description of the change
Changesets live in `.changeset/` as markdown files. Bump types: `patch` / `minor` / `major`.
One changeset can cover multiple packages.
### Phase 1: Prepare (cut release branch)
### Release Steps
- **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
1. `proman bump` — consume changesets and bump versions
2. `proman publish` — build → test → check → publish → changelog → tag → push
### 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
The publish command handles everything: workspace dependency resolution, npm publish order (core → fs → cli), changelog generation, git tagging, and pushing.
### 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
- **`workspace:*`** is auto-resolved by pnpm during publish
- **CHANGELOG** only contains official releases
+12 -10
View File
@@ -7,11 +7,13 @@ Every node has a typed payload: its `type` field is the hash of a JSON Schema th
## Install
```bash
bun add -g @ocas/cli
pnpm add -g @ocas/cli
```
The store is auto-created and bootstrapped on first use — no `init` command needed.
> Requires Node.js >= 22.5.0 (uses built-in node:sqlite)
## Quick Start
```bash
@@ -190,8 +192,8 @@ Nodes reachable from any variable binding are kept; everything else is swept.
## Using as a Library
```bash
bun add @ocas/core # in-memory store
bun add @ocas/core @ocas/fs # + filesystem persistence
pnpm add @ocas/core # in-memory store
pnpm add @ocas/core @ocas/fs # + filesystem persistence
```
```typescript
@@ -214,9 +216,9 @@ const node = store.get(hash);
For filesystem persistence, use `@ocas/fs`:
```typescript
import { openStoreAndVarStore } from "@ocas/fs";
import { openStore } from "@ocas/fs";
const { store, varStore } = await openStoreAndVarStore("/path/to/store");
const store = await openStore("/path/to/store");
```
See individual package READMEs for full API docs:
@@ -228,11 +230,11 @@ See individual package READMEs for full API docs:
```bash
git clone <repo-url> && cd ocas
bun install --no-cache
bun run build # tsc --build
bun test # run all tests
bun run check # biome lint
bun run format # biome format
pnpm install
pnpm run build # tsc --build
pnpm test # run all tests
pnpm run check # biome lint
pnpm run format # biome format
```
## License
-319
View File
@@ -1,319 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@uncaged/json-cas-workspace",
"devDependencies": {
"@biomejs/biome": "^2.0.0",
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.31.0",
"bun-types": "^1.3.14",
"typescript": "^5.8.0",
"ulidx": "^2.4.1",
},
},
"packages/cli": {
"name": "@ocas/cli",
"version": "0.1.1",
"bin": {
"ocas": "src/index.ts",
},
"dependencies": {
"@ocas/core": "0.1.1",
"@ocas/fs": "0.1.1",
},
},
"packages/core": {
"name": "@ocas/core",
"version": "0.1.1",
"dependencies": {
"ajv": "^8.20.0",
"cborg": "^4.2.3",
"liquidjs": "^10.27.0",
"xxhash-wasm": "^1.1.0",
},
},
"packages/fs": {
"name": "@ocas/fs",
"version": "0.1.1",
"dependencies": {
"@ocas/core": "0.1.1",
"cborg": "^4.2.3",
},
},
},
"packages": {
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="],
"@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="],
"@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="],
"@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="],
"@changesets/changelog-github": ["@changesets/changelog-github@0.7.0", "", { "dependencies": { "@changesets/get-github-info": "^0.8.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-rBsbRvc4TVn+FvFnOVM3LxlFJfTXXCp8gfVJ+0BubxWNSVnLuAzowi5j+IEraLLP52w8AAs9QfKbPS3MMiXQJA=="],
"@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="],
"@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="],
"@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="],
"@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="],
"@changesets/get-github-info": ["@changesets/get-github-info@0.8.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ=="],
"@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.10", "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="],
"@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="],
"@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="],
"@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="],
"@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="],
"@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="],
"@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="],
"@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="],
"@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="],
"@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="],
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
"@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="],
"@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@ocas/cli": ["@ocas/cli@workspace:packages/cli"],
"@ocas/core": ["@ocas/core@workspace:packages/core"],
"@ocas/fs": ["@ocas/fs@workspace:packages/fs"],
"@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"cborg": ["cborg@4.5.8", "", { "bin": { "cborg": "lib/bin.js" } }, "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw=="],
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="],
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
"dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="],
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="],
"is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"layerr": ["layerr@3.0.0", "", {}, "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA=="],
"liquidjs": ["liquidjs@10.27.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": { "liquidjs": "bin/liquid.js", "liquid": "bin/liquid.js" } }, "sha512-tw/OA59K7aIBlMKIrKlumr37fiZUheShVHXY8cVctWisgY1p9mc5hreOvlreoS0wTiwlWk14Ya7305c2a/Cg5w=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="],
"p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ulidx": ["ulidx@2.4.1", "", { "dependencies": { "layerr": "^3.0.0" } }, "sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="],
"@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="],
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
}
}
+4 -4
View File
@@ -17,8 +17,8 @@ The root README should have these sections in order:
4. **Packages** — table with ALL packages from packages/ directory, columns: Package, Description, Type (cli/lib)
5. **Quick Start** — install, build, basic usage
6. **CLI Reference** — brief command list, detailed usage in cli README
7. **Development**bun install / build / check / test
8. **Publishing**changeset workflow (bun run release)
7. **Development**pnpm install / build / check / test
8. **Publishing**`proman bump` + `proman publish`
## Per-Package README Structure
@@ -27,7 +27,7 @@ Each package README should have:
1. **Title** — package name
2. **One-line description** — matching package.json
3. **Overview** — what it does, where it sits in the architecture, dependencies
4. **Installation**bun add (for libs) or "included as binary" (for cli)
4. **Installation**pnpm add (for libs) or "included as binary" (for cli)
5. **API** (lib packages) — all exports from src/index.ts with type signatures, grouped by category, minimal usage examples
6. **CLI Usage** (cli packages) — command reference with examples
7. **Internal Structure** — brief src/ file organization
@@ -57,7 +57,7 @@ For each package read:
- All relative links work
- Package names match package.json
- No references to removed/renamed packages
- bun run build still passes
- pnpm run build still passes
## Guidelines
+21 -14
View File
@@ -1,30 +1,37 @@
{
"name": "@ocas/workspace",
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"@biomejs/biome": "^2.0.0",
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.31.0",
"bun-types": "^1.3.14",
"typescript": "^5.8.0",
"ulidx": "^2.4.1"
"@biomejs/biome": "^2.4.16",
"@shazhou/proman": "0.4.2",
"@types/node": "^25.9.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"ulidx": "^2.4.1",
"vite": "^8.0.16",
"vitest": "^4.1.8"
},
"scripts": {
"build": "tsc --build packages/core packages/fs",
"test": "bun test",
"check": "biome check .",
"format": "biome format --write .",
"release": "changeset version && bun run build && changeset publish"
"build": "proman build",
"test": "proman test",
"check": "proman check",
"format": "proman format"
},
"repository": {
"type": "git",
"url": "https://github.com/shazhou-ww/ocas.git"
},
"homepage": "https://github.com/shazhou-ww/ocas",
"engines": {
"node": ">=22.5.0"
},
"bugs": {
"url": "https://github.com/shazhou-ww/ocas/issues"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
],
"overrides": {}
}
}
+6
View File
@@ -1,5 +1,11 @@
# @ocas/cli
## 0.3.1
### Patch Changes
- Fix prompt docs: `bun``pnpm` install instructions, remove stale `--var-db` flag from usage docs.
## 0.2.0
### Breaking Changes
+4 -4
View File
@@ -15,17 +15,17 @@ The store is **auto-created and bootstrapped** on first use, so there is no `ini
Published as an npm package with a binary entry:
```bash
bun add -g @ocas/cli
pnpm add -g @ocas/cli
# or from the monorepo workspace:
bun link
pnpm link
```
**Binary name:** `ocas` (points to `src/index.ts`, run with Bun).
**Binary name:** `ocas` (points to `dist/index.js`, run with Node).
In development:
```bash
bun packages/cli-ocas/src/index.ts <command> [args]
node packages/cli/dist/index.js <command> [args]
```
## CLI Usage
+20 -8
View File
@@ -1,17 +1,26 @@
{
"name": "@ocas/cli",
"version": "0.2.0",
"version": "0.3.1",
"description": "CLI for OCAS content-addressed store",
"keywords": [
"cas",
"cli",
"content-addressing"
],
"engines": {
"node": ">=22.5.0"
},
"type": "module",
"bin": {
"ocas": "src/index.ts"
},
"scripts": {
"test": "bun test",
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
"ocas": "dist/index.js"
},
"files": [
"dist",
"prompts"
],
"dependencies": {
"@ocas/core": "0.2.0",
"@ocas/fs": "0.2.0"
"@ocas/core": "workspace:*",
"@ocas/fs": "workspace:*"
},
"repository": {
"type": "git",
@@ -21,5 +30,8 @@
"homepage": "https://github.com/shazhou-ww/ocas/tree/main/packages/cli",
"bugs": {
"url": "https://github.com/shazhou-ww/ocas/issues"
},
"devDependencies": {
"@types/node": "^25.9.1"
}
}
@@ -9,7 +9,7 @@ so that you know how to use the `ocas` CLI.
```bash
ocas --help
```
If not installed: `bun add -g @ocas/cli`
If not installed: `pnpm 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
@@ -6,7 +6,7 @@ OCAS is a self-describing content-addressable store for typed JSON data. Every n
All commands output `{ type, value }` JSON envelopes, making them composable via pipes.
**Install:** `bun add -g @ocas/cli`
**Install:** `pnpm add -g @ocas/cli`
**Packages:** `@ocas/core` (engine) · `@ocas/fs` (filesystem store) · `@ocas/cli` (CLI)
@@ -138,7 +138,6 @@ ocas gc | ocas render -p # human-readable stats
| 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 |
+13 -9
View File
@@ -1,8 +1,12 @@
#!/usr/bin/env bun
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
import type { Hash, ListEntry, ListOptions, Store, TagOp } from "@ocas/core";
import {
applyListOptions,
@@ -91,7 +95,7 @@ const { flags, positional } = parseArgs(process.argv.slice(2));
// --- Handle --version early ---
if (flags.version === true) {
const pkgPath = join(import.meta.dir, "..", "package.json");
const pkgPath = join(__dirname, "..", "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
process.stdout.write(`${pkg.version}\n`);
process.exit(0);
@@ -1033,11 +1037,11 @@ async function cmdList(_args: string[]): Promise<void> {
// Get all entries of the requested type (no limit/offset yet) and filter.
const allOfType = store.cas.listByType(typeHash, {
sort: opts.sort,
desc: opts.desc,
...(opts.sort !== undefined ? { sort: opts.sort } : {}),
...(opts.desc !== undefined ? { desc: opts.desc } : {}),
});
const filtered: ListEntry[] = allOfType.filter((e) =>
intersection!.has(e.hash),
intersection?.has(e.hash),
);
const paged = applyListOptions(filtered, opts);
await out(await wrapEnvelope(store, "@ocas/output/list", paged), store);
@@ -1064,7 +1068,7 @@ async function cmdListSchema(_args: string[]): Promise<void> {
}
function printUsage(): void {
const pkgPath = join(import.meta.dir, "..", "package.json");
const pkgPath = join(__dirname, "..", "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
console.log(`\
Usage: ocas [--home <path>] [--json] <command> [args]
@@ -1230,7 +1234,7 @@ switch (cmd) {
switch (sub) {
case "usage": {
const content = readFileSync(
join(import.meta.dir, "prompts", "usage.md"),
join(__dirname, "..", "prompts", "usage.md"),
"utf-8",
);
process.stdout.write(content);
@@ -1238,7 +1242,7 @@ switch (cmd) {
}
case "setup": {
const content = readFileSync(
join(import.meta.dir, "prompts", "setup.md"),
join(__dirname, "..", "prompts", "setup.md"),
"utf-8",
);
process.stdout.write(content);
@@ -1,60 +1,6 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Phase 7: Edge Cases 7.1 get non-existent hash errors gracefully 1`] = `"Node not found: AAAAAAAAAAAAA"`;
exports[`Phase 7: Edge Cases 7.3 var set empty name errors 1`] = `"Usage: ocas var set <name> <hash> [--tag <tag>...]"`;
exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Name must follow @scope/name format (e.g. @myapp/config)"`;
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
"Usage: ocas [--home <path>] [--json] <command> [args]
All JSON commands emit a { type, value } envelope. The type is the hash of the
command's @ocas/output/* schema (shown in parentheses); pipe any envelope into
\`render -p\` to render its value (ocas_ref hashes are expanded).
Commands:
put <type-hash> <file.json|--pipe> Store node, print envelope (value=hash) (@ocas/output/put)
get <hash> Print node as envelope (@ocas/output/get)
has <hash> Print envelope (value=boolean) (@ocas/output/has)
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@ocas/output/verify)
refs <hash> List direct ocas_ref edges (@ocas/output/refs)
walk <hash> [--format tree] Recursive traversal (@ocas/output/walk)
hash <type-hash> <file.json|--pipe> Compute hash without storing (@ocas/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-name> [--tag <tag>...] List hashes for a type, optionally filtered by tags (@ocas/output/list)
list-meta List meta-schema hashes (value=string[]) (@ocas/output/list-meta)
list-schema List all schema hashes (value=string[]) (@ocas/output/list-schema)
tag <target> <tag>... Apply tags/labels to a target (@ocas/output/tag)
untag <target> <tag>... Remove tags/labels from a target (@ocas/output/untag)
var set <name> <hash> [--tag <tag>...] Create/update a variable (@ocas/output/var-set)
var get <name> --schema <hash> Get a variable by name + schema (@ocas/output/var-get)
var delete <name> [--schema <hash>] Delete variable(s) (@ocas/output/var-delete)
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@ocas/output/var-list)
var history <name> [--schema <hash>] Show value history (LRU) (@ocas/output/var-history)
template set <schema-hash> <file> | --inline <text> Set template for schema (@ocas/output/template-set)
template get <schema-hash> Get template content (value=string) (@ocas/output/template-get)
template list List all templates (@ocas/output/template-list)
template delete <schema-hash> Delete template for schema (@ocas/output/template-delete)
gc Run garbage collection (@ocas/output/gc)
Flags:
--home <path> Store directory (default: $OCAS_HOME or ~/.ocas)
--json Compact JSON output
--render, -r Render output inline (equivalent to | ocas render -p)
--schema <hash> Schema hash filter for var get/delete/tag/list
--tag <tag> Tag/label (can be repeated): key:value (tag), name (label), :name (delete)
--inline <text> Inline text content for template set
--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)
Agent: If you have not installed the ocas skill for this version (0.2.0), run \`ocas prompt setup\` and follow the instructions."
`;
exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
exports[`Phase 3: Variable System > 3.1 var set creates variable 1`] = `
{
"type": "0Q5EMYK4SYSS9",
"value": {
@@ -67,7 +13,7 @@ exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
}
`;
exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
exports[`Phase 3: Variable System > 3.2 var get returns variable 1`] = `
{
"type": "F5RRJTXP8Z99D",
"value": {
@@ -80,7 +26,7 @@ exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
}
`;
exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
exports[`Phase 3: Variable System > 3.3 var list shows all variables 1`] = `
{
"type": "AF0XACGXHPMC1",
"value": [
@@ -312,7 +258,7 @@ 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`] = `
exports[`Phase 3: Variable System > 3.4 var list prefix filters by prefix 1`] = `
{
"type": "AF0XACGXHPMC1",
"value": [
@@ -327,7 +273,7 @@ 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`] = `
exports[`Phase 3: Variable System > 3.5 var set upsert updates existing variable 1`] = `
{
"type": "0Q5EMYK4SYSS9",
"value": {
@@ -340,7 +286,7 @@ exports[`Phase 3: Variable System 3.5 var set upsert updates existing variable 1
}
`;
exports[`Phase 3: Variable System 3.6 var set with tag and label adds them 1`] = `
exports[`Phase 3: Variable System > 3.6 var set with tag and label adds them 1`] = `
{
"type": "0Q5EMYK4SYSS9",
"value": {
@@ -357,7 +303,7 @@ exports[`Phase 3: Variable System 3.6 var set with tag and label adds them 1`] =
}
`;
exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag 1`] = `
exports[`Phase 3: Variable System > 3.7 var list --tag env:prod filters by kv tag 1`] = `
{
"type": "AF0XACGXHPMC1",
"value": [
@@ -376,7 +322,7 @@ 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`] = `
exports[`Phase 3: Variable System > 3.8 var list --tag important filters by label 1`] = `
{
"type": "AF0XACGXHPMC1",
"value": [
@@ -395,7 +341,7 @@ exports[`Phase 3: Variable System 3.8 var list --tag important filters by label
}
`;
exports[`Phase 3: Variable System 3.9 var set without label removes it 1`] = `
exports[`Phase 3: Variable System > 3.9 var set without label removes it 1`] = `
{
"type": "0Q5EMYK4SYSS9",
"value": {
@@ -410,7 +356,7 @@ exports[`Phase 3: Variable System 3.9 var set without label removes it 1`] = `
}
`;
exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
exports[`Phase 3: Variable System > 3.10 var delete removes variable 1`] = `
{
"type": "C3MYPR5RGQFZT",
"value": [
@@ -427,9 +373,9 @@ exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
}
`;
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=@myapp/config, schema=FRBAB1BF0ZBCS"`;
exports[`Phase 3: Variable System > 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=@myapp/config, schema=FRBAB1BF0ZBCS"`;
exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
exports[`Phase 4: Template System > 4.1 template set registers template 1`] = `
{
"type": "BJDHPAE4Q8TXM",
"value": {
@@ -439,14 +385,14 @@ exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
}
`;
exports[`Phase 4: Template System 4.2 template get returns template text 1`] = `
exports[`Phase 4: Template System > 4.2 template get returns template text 1`] = `
{
"type": "0B0HBHZGYHR84",
"value": "Name: {{ payload.name }}, Age: {{ payload.age }}",
}
`;
exports[`Phase 4: Template System 4.3 template list shows registered templates 1`] = `
exports[`Phase 4: Template System > 4.3 template list shows registered templates 1`] = `
{
"type": "8917JQTD1R5JF",
"value": [
@@ -458,7 +404,7 @@ 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`] = `
exports[`Phase 4: Template System > 4.4 template delete removes template 1`] = `
{
"type": "BY7BGZJND3N7R",
"value": {
@@ -467,4 +413,58 @@ exports[`Phase 4: Template System 4.4 template delete removes template 1`] = `
}
`;
exports[`Phase 4: Template System 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: FRBAB1BF0ZBCS"`;
exports[`Phase 4: Template System > 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: FRBAB1BF0ZBCS"`;
exports[`Phase 7: Edge Cases > 7.1 get non-existent hash errors gracefully 1`] = `"Node not found: AAAAAAAAAAAAA"`;
exports[`Phase 7: Edge Cases > 7.3 var set empty name errors 1`] = `"Usage: ocas var set <name> <hash> [--tag <tag>...]"`;
exports[`Phase 7: Edge Cases > 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Name must follow @scope/name format (e.g. @myapp/config)"`;
exports[`Phase 7: Edge Cases > 7.5 no subcommand shows help text 1`] = `
"Usage: ocas [--home <path>] [--json] <command> [args]
All JSON commands emit a { type, value } envelope. The type is the hash of the
command's @ocas/output/* schema (shown in parentheses); pipe any envelope into
\`render -p\` to render its value (ocas_ref hashes are expanded).
Commands:
put <type-hash> <file.json|--pipe> Store node, print envelope (value=hash) (@ocas/output/put)
get <hash> Print node as envelope (@ocas/output/get)
has <hash> Print envelope (value=boolean) (@ocas/output/has)
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@ocas/output/verify)
refs <hash> List direct ocas_ref edges (@ocas/output/refs)
walk <hash> [--format tree] Recursive traversal (@ocas/output/walk)
hash <type-hash> <file.json|--pipe> Compute hash without storing (@ocas/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-name> [--tag <tag>...] List hashes for a type, optionally filtered by tags (@ocas/output/list)
list-meta List meta-schema hashes (value=string[]) (@ocas/output/list-meta)
list-schema List all schema hashes (value=string[]) (@ocas/output/list-schema)
tag <target> <tag>... Apply tags/labels to a target (@ocas/output/tag)
untag <target> <tag>... Remove tags/labels from a target (@ocas/output/untag)
var set <name> <hash> [--tag <tag>...] Create/update a variable (@ocas/output/var-set)
var get <name> --schema <hash> Get a variable by name + schema (@ocas/output/var-get)
var delete <name> [--schema <hash>] Delete variable(s) (@ocas/output/var-delete)
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@ocas/output/var-list)
var history <name> [--schema <hash>] Show value history (LRU) (@ocas/output/var-history)
template set <schema-hash> <file> | --inline <text> Set template for schema (@ocas/output/template-set)
template get <schema-hash> Get template content (value=string) (@ocas/output/template-get)
template list List all templates (@ocas/output/template-list)
template delete <schema-hash> Delete template for schema (@ocas/output/template-delete)
gc Run garbage collection (@ocas/output/gc)
Flags:
--home <path> Store directory (default: $OCAS_HOME or ~/.ocas)
--json Compact JSON output
--render, -r Render output inline (equivalent to | ocas render -p)
--schema <hash> Schema hash filter for var get/delete/tag/list
--tag <tag> Tag/label (can be repeated): key:value (tag), name (label), :name (delete)
--inline <text> Inline text content for template set
--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)
Agent: If you have not installed the ocas skill for this version (0.3.1), run \`ocas prompt setup\` and follow the instructions."
`;
@@ -1,6 +1,6 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Phase 1: CAS Core 1.6 get returns node JSON (snapshot) 1`] = `
exports[`Phase 1: CAS Core > 1.6 get returns node JSON (snapshot) 1`] = `
{
"type": "7V5G8E2VW8B2G",
"value": {
@@ -1,5 +1,5 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Phase 5: Render 5.1 render fills payload variables 1`] = `"Hello Alice!"`;
exports[`Phase 5: Render > 5.1 render fills payload variables 1`] = `"Hello Alice!"`;
exports[`Phase 5: Render 5.2 render --resolution with different value 1`] = `"Hello Alice!"`;
exports[`Phase 5: Render > 5.2 render --resolution with different value 1`] = `"Hello Alice!"`;
@@ -1,3 +1,3 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Phase 2: Schema Validation 2.3 put against non-existent schema hash fails 1`] = `"Schema not found: AAAAAAAAAAAAA"`;
exports[`Phase 2: Schema Validation > 2.3 put against non-existent schema hash fails 1`] = `"Schema not found: AAAAAAAAAAAAA"`;
@@ -1,20 +1,20 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Phase 1: CAS Core 1.9 verify returns ok for valid node 1`] = `
exports[`Phase 1: CAS Core > 1.9 verify returns ok for valid node 1`] = `
{
"type": "52HEFB52BD0GF",
"value": "ok",
}
`;
exports[`Phase 1: CAS Core 1.10 refs lists direct references (snapshot) 1`] = `
exports[`Phase 1: CAS Core > 1.10 refs lists direct references (snapshot) 1`] = `
"{
"type": "2TKP4RGBJ4V43",
"value": []
}"
`;
exports[`Phase 1: CAS Core 1.11 walk shows traversal tree (snapshot) 1`] = `
exports[`Phase 1: CAS Core > 1.11 walk shows traversal tree (snapshot) 1`] = `
"{
"type": "4HG6MD3XG5H5C",
"value": [
+24 -24
View File
@@ -1,7 +1,8 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
// ---- @ Alias Resolution Tests ----
@@ -16,7 +17,7 @@ beforeEach(() => {
`ocas-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
@@ -34,31 +35,30 @@ afterEach(() => {
/**
* Run CLI command and return stdout, stderr, and exit code
*/
async function runCliAlias(...args: string[]): Promise<{
function runCliAlias(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
/** Extract the `value` field from a { type, value } envelope JSON string. */
+102 -55
View File
@@ -1,35 +1,33 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue, stripVolatile } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const pkgPath = resolve(import.meta.dir, "../package.json");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
const pkgPath = resolve(import.meta.dirname, "../package.json");
// --- ocas command alias tests (from cli.test.ts) ---
describe("ocas binary", () => {
test("T1: ocas bin entry exists in package.json", async () => {
const pkg = await Bun.file(pkgPath).json();
expect(pkg.bin.ocas).toBe("src/index.ts");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
expect(pkg.bin.ocas).toBe("dist/index.js");
});
test("T2: no legacy bin entries (json-cas, ucas)", async () => {
const pkg = await Bun.file(pkgPath).json();
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
expect(pkg.bin["json-cas"]).toBeUndefined();
expect(pkg.bin.ucas).toBeUndefined();
expect(Object.keys(pkg.bin)).toEqual(["ocas"]);
});
test("T3: ocas command is executable and shows help", async () => {
const proc = Bun.spawn(["bun", entrypoint, "--help"], {
stdout: "pipe",
stderr: "pipe",
test("T3: ocas command is executable and shows help", () => {
const stdout = execFileSync("node", [entrypoint, "--help"], {
encoding: "utf-8",
timeout: 10000,
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
expect(exitCode).toBe(0);
expect(stdout.length).toBeGreaterThan(0);
});
});
@@ -41,17 +39,31 @@ describe("Phase 7: Edge Cases", () => {
let typeHash: string;
let nodeHash: string;
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
beforeAll(async () => {
@@ -125,17 +137,24 @@ describe("Phase 7: Edge Cases", () => {
expect(combined.toLowerCase()).toContain("usage");
});
test("7.6 --home path is a file errors", async () => {
test("7.6 --home path is a file errors", () => {
const fileAsStore = join(tmpStore, "not-a-directory");
writeFileSync(fileAsStore, "test");
const proc = Bun.spawn(
["bun", entrypoint, "--home", fileAsStore, "get", "AAAAAAAAAAAAA"],
{ stdout: "pipe", stderr: "pipe" },
);
const exitCode = await proc.exited;
const stderr = (await new Response(proc.stderr).text()).trim();
expect(exitCode).not.toBe(0);
expect(stderr).toContain("not a directory");
try {
execFileSync(
"node",
[entrypoint, "--home", fileAsStore, "get", "AAAAAAAAAAAAA"],
{
encoding: "utf-8",
timeout: 10000,
},
);
expect.unreachable("should have thrown");
} catch (e: unknown) {
const err = e as { stderr?: string; status?: number };
expect(err.status).not.toBe(0);
expect((err.stderr ?? "").trim()).toContain("not a directory");
}
});
});
@@ -146,17 +165,31 @@ describe("Phase 3: Variable System", () => {
let typeHash: string;
let nodeHash: string;
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
beforeAll(async () => {
@@ -335,17 +368,31 @@ describe("Phase 4: Template System", () => {
let tmpStore: string;
let typeHash: string;
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
beforeAll(async () => {
+38 -24
View File
@@ -1,10 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
let tmpStore: string;
let typeHash: string;
@@ -41,17 +42,30 @@ afterAll(() => {
rmSync(tmpStore, { recursive: true, force: true });
});
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
// ---- Phase 6: GC ----
@@ -71,19 +85,19 @@ describe("Phase 6: GC", () => {
);
});
test("6.2 gc | render -p renders the gc stats", async () => {
const { stdout: gcOut, exitCode: gcExit } = await runCli(["gc"]);
test("6.2 gc | render -p renders the gc stats", () => {
const { stdout: gcOut, exitCode: gcExit } = runCli(["gc"]);
expect(gcExit).toBe(0);
const proc = Bun.spawn(
["bun", entrypoint, "--home", tmpStore, "render", "--pipe"],
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
);
proc.stdin.write(gcOut);
proc.stdin.end();
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
expect(exitCode).toBe(0);
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, "render", "--pipe"],
{
input: gcOut,
encoding: "utf-8",
timeout: 10000,
},
).trim();
// gc value is an object { total, reachable, collected, scanned }
expect(stdout).toContain("total:");
});
+24 -18
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash } from "@ocas/core";
import { bootstrap, validate } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
let testDir: string;
let storePath: string;
@@ -16,7 +17,7 @@ beforeEach(() => {
`ocas-get-tag-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
});
@@ -29,25 +30,30 @@ afterEach(() => {
}
});
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{ stdout: "pipe", stderr: "pipe" },
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function createTestNode(value = "hello-world"): Promise<Hash> {
+43 -28
View File
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import {
mkdirSync,
mkdtempSync,
@@ -22,8 +23,8 @@ export {
writeFileSync,
};
export const entrypoint = resolve(import.meta.dir, "../src/index.ts");
export const pkgPath = resolve(import.meta.dir, "../package.json");
export const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
export const pkgPath = resolve(import.meta.dirname, "../package.json");
/** Extract the `value` field from a { type, value } envelope JSON string. */
export function envValue(json: string): unknown {
@@ -46,44 +47,58 @@ export async function putSchemaFile(
return hash;
}
const quietEnv = { ...process.env, NODE_NO_WARNINGS: "1" };
/**
* Run CLI command. Accepts either a string[] or ...string[] (rest args).
* If first arg is an array, uses that as args. Otherwise treats all args as the command.
*/
export async function runCli(
export function runCli(
args: string[],
storePath?: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
): { stdout: string; stderr: string; exitCode: number } {
const finalArgs = storePath
? ["bun", entrypoint, "--home", storePath, ...args]
: ["bun", entrypoint, ...args];
const proc = Bun.spawn(finalArgs, {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { stdout, stderr, exitCode };
? [entrypoint, "--home", storePath, ...args]
: [entrypoint, ...args];
try {
const stdout = execFileSync("node", finalArgs, {
encoding: "utf-8",
timeout: 10000,
env: quietEnv,
});
return { stdout, stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: err.stdout ?? "",
stderr: err.stderr ?? "",
exitCode: err.status ?? 1,
};
}
}
export async function runCliWithStdin(
export function runCliWithStdin(
args: string[],
storePath: string,
stdin: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const finalArgs = ["bun", entrypoint, "--home", storePath, ...args];
const proc = Bun.spawn(finalArgs, {
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
});
proc.stdin.write(stdin);
proc.stdin.end();
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { stdout, stderr, exitCode };
): { stdout: string; stderr: string; exitCode: number } {
const finalArgs = [entrypoint, "--home", storePath, ...args];
try {
const stdout = execFileSync("node", finalArgs, {
input: stdin,
encoding: "utf-8",
timeout: 10000,
env: quietEnv,
});
return { stdout, stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: err.stdout ?? "",
stderr: err.stderr ?? "",
exitCode: err.status ?? 1,
};
}
}
/**
+1 -1
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BOOTSTRAP_STORE } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { envValue, runCli } from "./helpers.js";
let storePath: string;
+11 -18
View File
@@ -1,7 +1,8 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { envValue, runCli } from "./helpers.js";
const HASH_RE = /^[0-9A-HJKMNP-TV-Z]{13}$/;
@@ -18,24 +19,16 @@ afterEach(() => {
});
async function putString(text: string): Promise<string> {
// Use the @ocas/string built-in via library; CLI doesn't expose put-text but
// `put @ocas/string --pipe` works. We'll use --pipe with stdin.
const proc = Bun.spawn(
[
"bun",
join(import.meta.dir, "../src/index.ts"),
"--home",
storePath,
"put",
"@ocas/string",
"--pipe",
],
{ stdout: "pipe", stderr: "pipe", stdin: "pipe" },
const entrypoint = join(import.meta.dirname, "../dist/index.js");
const out = execFileSync(
"node",
[entrypoint, "--home", storePath, "put", "@ocas/string", "--pipe"],
{
input: JSON.stringify(text),
encoding: "utf-8",
timeout: 10000,
},
);
proc.stdin.write(JSON.stringify(text));
proc.stdin.end();
await proc.exited;
const out = await new Response(proc.stdout).text();
return (JSON.parse(out) as { value: string }).value;
}
+39 -40
View File
@@ -1,7 +1,8 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
let testDir: string;
let storePath: string;
@@ -13,7 +14,7 @@ beforeEach(() => {
`ocas-list-tag-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
});
@@ -26,50 +27,48 @@ afterEach(() => {
}
});
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{ stdout: "pipe", stderr: "pipe" },
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function putString(value: string): Promise<string> {
const proc = Bun.spawn(
[
"bun",
"run",
cliPath,
"--home",
storePath,
"put",
"@ocas/string",
"--pipe",
],
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
);
proc.stdin.write(JSON.stringify(value));
await proc.stdin.end();
const out = await new Response(proc.stdout).text();
const err = await new Response(proc.stderr).text();
await proc.exited;
if ((proc.exitCode ?? 0) !== 0) {
throw new Error(`put failed: ${err}`);
function putString(value: string): string {
try {
const out = execFileSync(
"node",
[cliPath, "--home", storePath, "put", "@ocas/string", "--pipe"],
{
input: JSON.stringify(value),
encoding: "utf-8",
timeout: 10000,
},
);
return JSON.parse(out.trim()).value as string;
} catch (e: unknown) {
const err = e as { stderr?: string };
throw new Error(`put failed: ${err.stderr ?? ""}`);
}
return JSON.parse(out.trim()).value as string;
}
async function tag(target: string, ...tagSpecs: string[]): Promise<void> {
+48 -26
View File
@@ -1,10 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
let tmpStore: string;
let typeHash: string;
@@ -43,34 +44,55 @@ afterAll(() => {
rmSync(tmpStore, { recursive: true, force: true });
});
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function runCliWithStdin(
function runCliWithStdin(
args: string[],
stdin: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write(stdin);
proc.stdin.end();
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
): { stdout: string; stderr: string; exitCode: number } {
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
input: stdin,
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
// ---- Phase 8: Pipe Composition ----
+26 -13
View File
@@ -1,10 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue, stripVolatile } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
let tmpStore: string;
let typeHash: string;
@@ -41,17 +42,29 @@ afterAll(() => {
rmSync(tmpStore, { recursive: true, force: true });
});
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(args: string[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
describe("Phase 1: CAS Core", () => {
+46 -24
View File
@@ -1,12 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { bootstrap } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue, putSchemaFile, runCli, runCliWithStdin } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
// --- Standalone render tests from cli.test.ts ---
@@ -57,34 +58,55 @@ describe("Phase 5: Render", () => {
let typeHash: string;
let nodeHash: string;
async function runCliE2e(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCliE2e(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function _runCliE2eWithStdin(
args: string[],
stdin: string,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write(stdin);
proc.stdin.end();
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
input: stdin,
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
beforeAll(async () => {
+47 -21
View File
@@ -1,7 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue, putSchemaFile, runCli } from "./helpers";
// ---- Issue #50: Schema Validation in put Command ----
@@ -559,7 +560,7 @@ describe("Phase 2: Schema Validation", () => {
let typeHash: string;
let _nodeHash: string;
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
beforeAll(async () => {
tmpStore = mkdtempSync(join(tmpdir(), "ocas-e2e-"));
@@ -581,12 +582,14 @@ describe("Phase 2: Schema Validation", () => {
const nodeFile = join(tmpStore, "test-node.json");
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
const proc = Bun.spawn(
["bun", entrypoint, "--home", tmpStore, "put", typeHash, nodeFile],
{ stdout: "pipe", stderr: "pipe" },
);
await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, "put", typeHash, nodeFile],
{
encoding: "utf-8",
timeout: 10000,
},
).trim();
_nodeHash = envValue(stdout) as string;
});
@@ -597,13 +600,25 @@ describe("Phase 2: Schema Validation", () => {
test("2.1 put {name:123} against string-schema fails with non-zero exit", async () => {
const badFile = join(tmpStore, "bad-node.json");
writeFileSync(badFile, JSON.stringify({ name: 123 }));
const proc = Bun.spawn(
["bun", entrypoint, "--home", tmpStore, "put", typeHash, badFile],
{ stdout: "pipe", stderr: "pipe" },
);
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
let stdout = "",
stderr = "",
exitCode = 0;
try {
stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, "put", typeHash, badFile],
{
encoding: "utf-8",
timeout: 10000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
},
).trim();
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
stdout = (err.stdout ?? "").trim();
stderr = (err.stderr ?? "").trim();
exitCode = err.status ?? 1;
}
expect(exitCode).not.toBe(0);
expect(stdout).toBe("");
expect(stderr).toContain("Validation failed");
@@ -612,12 +627,23 @@ describe("Phase 2: Schema Validation", () => {
test("2.3 put against non-existent schema hash fails", async () => {
const nodeFile = join(tmpStore, "test-node.json");
const proc = Bun.spawn(
["bun", entrypoint, "--home", tmpStore, "put", "AAAAAAAAAAAAA", nodeFile],
{ stdout: "pipe", stderr: "pipe" },
);
const exitCode = await proc.exited;
const stderr = (await new Response(proc.stderr).text()).trim();
let exitCode = 0,
stderr = "";
try {
execFileSync(
"node",
[entrypoint, "--home", tmpStore, "put", "AAAAAAAAAAAAA", nodeFile],
{
encoding: "utf-8",
timeout: 10000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
},
);
} catch (e: unknown) {
const err = e as { stderr?: string; status?: number };
stderr = (err.stderr ?? "").trim();
exitCode = err.status ?? 1;
}
expect(exitCode).not.toBe(0);
expect(stderr).toMatchSnapshot();
});
+24 -18
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash } from "@ocas/core";
import { bootstrap } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
let testDir: string;
let storePath: string;
@@ -16,7 +17,7 @@ beforeEach(() => {
`ocas-tag-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
});
@@ -29,25 +30,30 @@ afterEach(() => {
}
});
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{ stdout: "pipe", stderr: "pipe" },
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function createTestNode(): Promise<Hash> {
+24 -24
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash, Store } from "@ocas/core";
import { bootstrap } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
// ---- Test helpers ----
@@ -19,7 +20,7 @@ beforeEach(() => {
`ocas-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
@@ -37,31 +38,30 @@ afterEach(() => {
/**
* Run CLI command and return stdout, stderr, and exit code
*/
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
/**
@@ -1,8 +1,8 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const usagePath = join(import.meta.dir, "usage.md");
const usagePath = join(import.meta.dirname, "..", "prompts", "usage.md");
describe("usage.md doc cleanup (D)", () => {
test("D3. usage.md does not reference legacy openStoreAndVarStore / createVariableStore", () => {
+24 -24
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash, Store } from "@ocas/core";
import { bootstrap } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
let testDir: string;
let storePath: string;
@@ -16,7 +17,7 @@ beforeEach(() => {
`ocas-history-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
@@ -30,31 +31,30 @@ afterEach(() => {
}
});
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
async function setupSchemaAndValues(): Promise<{
+29 -41
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash, Store } from "@ocas/core";
import { bootstrap, putSchema } from "@ocas/core";
import { openStore as openFsStore } from "@ocas/fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
// ---- Test helpers ----
@@ -19,7 +20,7 @@ beforeEach(() => {
`ocas-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storePath = join(testDir, "store");
cliPath = join(import.meta.dir, "../src/index.ts");
cliPath = join(import.meta.dirname, "../dist/index.js");
mkdirSync(testDir, { recursive: true });
mkdirSync(storePath, { recursive: true });
@@ -37,31 +38,30 @@ afterEach(() => {
/**
* Run CLI command and return stdout, stderr, and exit code
*/
async function runCli(...args: string[]): Promise<{
function runCli(...rawArgs: (string | string[])[]): {
stdout: string;
stderr: string;
exitCode: number;
}> {
const proc = Bun.spawn(
["bun", "run", cliPath, "--home", storePath, ...args],
{
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: proc.exitCode ?? 0,
};
} {
const args = rawArgs.flat();
try {
const stdout = execFileSync(
"node",
[cliPath, "--home", storePath, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
/**
@@ -783,26 +783,14 @@ describe("global options", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Override with custom store path
const proc = Bun.spawn(
[
"bun",
"run",
cliPath,
"--home",
customStorePath,
"var",
"set",
"@test/x",
hash,
],
execFileSync(
"node",
[cliPath, "--home", customStorePath, "var", "set", "@test/x", hash],
{
stdout: "pipe",
stderr: "pipe",
encoding: "utf-8",
timeout: 10000,
},
);
await proc.exited;
expect(proc.exitCode).toBe(0);
});
});
+26 -13
View File
@@ -1,10 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { envValue, stripVolatile } from "./helpers";
const entrypoint = resolve(import.meta.dir, "../src/index.ts");
const entrypoint = resolve(import.meta.dirname, "../dist/index.js");
let tmpStore: string;
let typeHash: string;
@@ -38,17 +39,29 @@ afterAll(() => {
rmSync(tmpStore, { recursive: true, force: true });
});
async function runCli(
args: string[],
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(["bun", entrypoint, "--home", tmpStore, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = (await new Response(proc.stdout).text()).trim();
const stderr = (await new Response(proc.stderr).text()).trim();
return { stdout, stderr, exitCode };
function runCli(args: string[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
try {
const stdout = execFileSync(
"node",
[entrypoint, "--home", tmpStore, ...args],
{
encoding: "utf-8",
timeout: 10000,
},
);
return { stdout: stdout.trim(), stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; status?: number };
return {
stdout: (err.stdout ?? "").trim(),
stderr: (err.stderr ?? "").trim(),
exitCode: err.status ?? 1,
};
}
}
describe("Phase 1: CAS Core", () => {
+5 -5
View File
@@ -1,10 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": false,
"declaration": false,
"declarationMap": false,
"noEmit": true
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
"include": ["src"],
"exclude": ["src/**/*.test.ts"],
"references": [{ "path": "../core" }, { "path": "../fs" }]
}
+4 -9
View File
@@ -6,14 +6,14 @@ Core CAS engine — hashing, schema, store, verify, bootstrap.
`@ocas/core` is the foundation of the ocas monorepo. It defines content-addressed nodes (`CasNode`), the `Store` interface, XXH64-based hashing with deterministic CBOR, JSON Schema registration and validation (including `cas_ref` links between nodes), bootstrap seeding, and integrity verification.
Other packages build on this layer: `ocas-fs` provides persistence, and `cli-ocas` exposes store operations on the command line.
Other packages build on this layer: `@ocas/fs` provides persistence, and `@ocas/cli` exposes store operations on the command line.
**Dependencies:** `ajv`, `cborg`, `xxhash-wasm`
**Dependencies:** `ajv`, `cborg`, `liquidjs`, `xxhash-wasm`
## Installation
```bash
bun add @ocas/core
pnpm add @ocas/core
```
## API
@@ -32,12 +32,7 @@ type CasNode<T = unknown> = {
timestamp: number; // Unix epoch ms
};
type Store = {
put(typeHash: Hash, payload: unknown): Promise<Hash>;
get(hash: Hash): CasNode | null;
has(hash: Hash): boolean;
listByType(typeHash: Hash): Hash[];
};
type Store = { cas: CasStore; var: VarStore; tag: TagStore; };
type JSONSchema = Record<string, unknown>;
+14 -5
View File
@@ -1,6 +1,16 @@
{
"name": "@ocas/core",
"version": "0.2.0",
"version": "0.3.0",
"description": "Core CAS engine — hashing, schema, store, verify, bootstrap",
"keywords": [
"cas",
"content-addressing",
"json-schema",
"typescript"
],
"engines": {
"node": ">=22.5.0"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -14,10 +24,6 @@
"dist",
"src"
],
"scripts": {
"test": "bun test",
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
},
"dependencies": {
"ajv": "^8.20.0",
"cborg": "^4.2.3",
@@ -32,5 +38,8 @@
"homepage": "https://github.com/shazhou-ww/ocas/tree/main/packages/core",
"bugs": {
"url": "https://github.com/shazhou-ww/ocas/issues"
},
"devDependencies": {
"@types/node": "^25.9.1"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import type { JSONSchema } from "./schema.js";
import { getSchema } from "./schema.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { gc } from "./gc.js";
import { putSchema } from "./schema.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { cborEncode } from "./cbor.js";
+21 -21
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { renderWithTemplate } from "./liquid-render.js";
import { putSchema } from "./schema.js";
@@ -598,7 +598,7 @@ describe("Suite 4: Render Flow Integration", () => {
epsilon: 0.01,
});
expect(output.length).toBe(0);
await expect(output.length).toBe(0);
} finally {
await cleanup();
}
@@ -623,13 +623,13 @@ describe("Suite 4: Render Flow Integration", () => {
);
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
await expect(async () => {
await renderWithTemplate(store, nodeHash, {
await expect(
renderWithTemplate(store, nodeHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
}).toThrow();
}),
).rejects.toThrow();
} finally {
await cleanup();
}
@@ -905,8 +905,8 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
});
expect(output).toContain("Item: item1");
expect(output).toContain("Item: item2");
expect(output).toContain("Item: item3");
await expect(output).toContain("Item: item2");
await expect(output).toContain("Item: item3");
} finally {
await cleanup();
}
@@ -937,7 +937,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
epsilon: 0.01,
});
expect(output).toBeDefined();
await expect(output).toBeDefined();
} finally {
await cleanup();
}
@@ -970,13 +970,13 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
);
store.var.set(`@ocas/template/text/${parentSchema}`, template);
await expect(async () => {
await renderWithTemplate(store, parentHash, {
await expect(
renderWithTemplate(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
}).toThrow(/decay/);
}),
).rejects.toThrow(/decay/);
} finally {
await cleanup();
}
@@ -1009,13 +1009,13 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
);
store.var.set(`@ocas/template/text/${parentSchema}`, template);
await expect(async () => {
await renderWithTemplate(store, parentHash, {
await expect(
renderWithTemplate(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
}).toThrow();
}),
).rejects.toThrow();
} finally {
await cleanup();
}
@@ -1048,13 +1048,13 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
);
store.var.set(`@ocas/template/text/${parentSchema}`, template);
await expect(async () => {
await renderWithTemplate(store, parentHash, {
await expect(
renderWithTemplate(store, parentHash, {
resolution: 1.0,
decay: 0.5,
epsilon: 0.01,
});
}).toThrow(/decay/);
}),
).rejects.toThrow(/decay/);
} finally {
await cleanup();
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { createMemoryStore } from "./store.js";
+2 -2
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
function* walk(dir: string): Generator<string> {
for (const name of readdirSync(dir)) {
@@ -16,7 +16,7 @@ function* walk(dir: string): Generator<string> {
describe("no SQLite in @ocas/core", () => {
test("source files do not import sqlite", () => {
const srcDir = import.meta.dir;
const srcDir = import.meta.dirname;
const needle = ["bun", "sqlite"].join(":");
for (const file of walk(srcDir)) {
if (!file.endsWith(".ts")) continue;
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { registerOutputTemplates } from "./output-templates.js";
import { createMemoryStore } from "./store.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { CasNodeNotFoundError } from "./errors.js";
import { render, renderAsync, renderDirect } from "./render.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { getSchema, putSchema, refs, validate, walk } from "./schema.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { createMemoryStore } from "./store.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { createMemoryStore } from "./store.js";
const T1 = "AAAAAAAAAAAAA";
+2 -2
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import type {
CasNode,
CasStore,
@@ -134,7 +134,7 @@ describe("Aggregate Store type", () => {
describe("source convention", () => {
test("types.ts uses 'type' not 'interface' for new types", () => {
const src = readFileSync(join(import.meta.dir, "types.ts"), "utf8");
const src = readFileSync(join(import.meta.dirname, "types.ts"), "utf8");
for (const name of ["CasStore", "VarStore", "TagStore"]) {
expect(src).toMatch(new RegExp(`export\\s+type\\s+${name}\\b`));
expect(src).not.toMatch(new RegExp(`interface\\s+${name}\\b`));
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import {
CasNodeNotFoundError,
InvalidVariableNameError,
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import type { Variable } from "./variable.js";
describe("Variable Type", () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "./bootstrap.js";
import { createMemoryStore } from "./store.js";
import { wrapEnvelope } from "./wrap-envelope.js";
+37 -35
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { bootstrap } from "../src/bootstrap.js";
import { MemStore } from "../src/mem-store.js";
import type { JSONSchema } from "../src/schema.js";
@@ -258,169 +258,171 @@ describe("Test Suite 3: putSchema Validation - Invalid Schemas", () => {
test("3.1: Reject schema with invalid type value", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () => putSchema(store, { type: "garbage" })).toThrow();
await expect(async () =>
putSchema(store, { type: "garbage" }),
).rejects.toThrow();
});
test("3.2: Reject schema with type as number", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, { type: 123 } as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.3: Reject schema with properties not an object", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "object",
properties: "not-an-object",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.4: Reject schema with required not an array", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "object",
required: "name",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.5: Reject schema with required containing non-strings", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "object",
required: ["name", 123, true],
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.6: Reject schema with additionalProperties as string", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "object",
additionalProperties: "yes",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.7: Reject schema with anyOf not an array", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
anyOf: { type: "string" },
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.8: Reject schema with empty anyOf array", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () => putSchema(store, { anyOf: [] })).toThrow();
await expect(async () => putSchema(store, { anyOf: [] })).rejects.toThrow();
});
test("3.9: Reject schema with items not an object", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "array",
items: "string",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.10: Reject schema with format not a string", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "string",
format: 123,
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.11: Reject schema with enum not an array", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "string",
enum: "red",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.12: Reject schema with empty enum array", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, { type: "string", enum: [] }),
).toThrow();
).rejects.toThrow();
});
test("3.13: Reject schema with title not a string", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "string",
title: 123,
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.14: Reject schema with description not a string", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "string",
description: ["not a string"],
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.15: Reject schema with unsupported $ref keyword", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
$ref: "#/definitions/user",
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.16: Reject completely invalid data (non-object)", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, "not-a-schema" as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
test("3.17: Reject nested invalid schema in properties", async () => {
const store = new MemStore();
bootstrap(store);
expect(async () =>
await expect(async () =>
putSchema(store, {
type: "object",
properties: {
name: { type: "invalid-type" },
},
} as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
});
@@ -641,9 +643,9 @@ describe("Test Suite 7: Meta-Schema Content Validation", () => {
expect(hash2).toBeTruthy();
// Invalid type (number)
expect(async () =>
await expect(async () =>
putSchema(store, { type: 123 } as unknown as JSONSchema),
).toThrow();
).rejects.toThrow();
});
});
+4
View File
@@ -1,5 +1,9 @@
# @ocas/fs
## 0.3.0 — 2026-06-03
- Migrate from better-sqlite3 to built-in node:sqlite — zero native addon dependencies, no more NODE_MODULE_VERSION mismatch across Node upgrades.
## 0.2.0
### Breaking Changes
+7 -20
View File
@@ -4,7 +4,7 @@ Filesystem-backed CAS store.
## Overview
`@ocas/fs` implements a persistent `Store` on disk. Each node is stored as `<hash>.bin` (CBOR-encoded `CasNode`). A `_index/` directory maps type hashes to content hashes for `listByType`. Stores support bootstrap via the same `BOOTSTRAP_STORE` symbol as the in-memory implementation.
`@ocas/fs` implements a persistent `Store` backed by `node:sqlite` (`DatabaseSync`). Nodes are stored as CBOR blobs in SQLite tables. Stores support bootstrap via the same `BOOTSTRAP_STORE` symbol as the in-memory implementation.
Depends on `@ocas/core` for hashing, CBOR encoding, and types.
@@ -13,7 +13,7 @@ Depends on `@ocas/core` for hashing, CBOR encoding, and types.
## Installation
```bash
bun add @ocas/fs
pnpm add @ocas/fs
```
## API
@@ -21,19 +21,18 @@ bun add @ocas/fs
Exported from `src/index.ts`:
```typescript
function createFsStore(dir: string): BootstrapCapableStore;
function openStore(path: string): Promise<Store>;
```
Returns a `BootstrapCapableStore` from `@ocas/core`. The store loads existing `.bin` files on open and migrates or builds the type index on first use.
Returns a unified `Store` with `cas`, `var`, and `tag` sub-stores, backed by SQLite. Bootstraps automatically on open.
### Example
```typescript
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import { putSchema } from "@ocas/core";
import { openStore } from "@ocas/fs";
const store = createFsStore("./my-cas-store");
await bootstrap(store);
const store = await openStore("./my-cas-store");
const typeHash = await putSchema(store, {
type: "object",
@@ -46,18 +45,6 @@ const hash = await store.put(typeHash, { id: "item-1" });
console.log(store.has(hash)); // true after restart if same dir
```
### On-disk layout
```
my-cas-store/
├── <hash>.bin # CBOR CasNode
├── _index/
│ └── <typeHash> # newline-separated content hashes
└── ...
```
Writes use atomic rename (`<hash>.tmp``<hash>.bin`).
## Internal Structure
| File | Purpose |
+16 -7
View File
@@ -1,6 +1,16 @@
{
"name": "@ocas/fs",
"version": "0.2.0",
"version": "0.3.0",
"description": "Filesystem-backed CAS store with SQLite",
"keywords": [
"cas",
"filesystem",
"sqlite",
"storage"
],
"engines": {
"node": ">=22.5.0"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -14,13 +24,9 @@
"dist",
"src"
],
"scripts": {
"test": "bun test",
"prepublishOnly": "bash ../../scripts/check-workspace-deps.sh"
},
"dependencies": {
"cborg": "^4.2.3",
"@ocas/core": "0.2.0"
"@ocas/core": "workspace:*",
"cborg": "^4.2.3"
},
"repository": {
"type": "git",
@@ -30,5 +36,8 @@
"homepage": "https://github.com/shazhou-ww/ocas/tree/main/packages/fs",
"bugs": {
"url": "https://github.com/shazhou-ww/ocas/issues"
},
"devDependencies": {
"@types/node": "^25.9.1"
}
}
+1
View File
@@ -1 +1,2 @@
export { createSqliteVarStore } from "./sqlite-store.js";
export { createFsStore, openStore, prepareStore } from "./store.js";
+686
View File
@@ -0,0 +1,686 @@
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import type {
CasStore,
Hash,
HistoryEntry,
ListOptions,
Tag,
TagOp,
TagStore,
Variable,
VarListOptions,
VarSetOptions,
VarStore,
} from "@ocas/core";
import {
addNameIndex,
checkTagLabelConflict,
extractSchema,
MAX_HISTORY,
removeNameIndex,
SchemaMismatchError,
VariableNotFoundError,
type VarRecord,
validateName,
varKey,
} from "@ocas/core";
function transaction<T>(db: DatabaseSync, fn: () => T): T {
db.exec("BEGIN");
try {
const r = fn();
db.exec("COMMIT");
return r;
} catch (e) {
db.exec("ROLLBACK");
throw e;
}
}
const DB_FILE = "_store.db";
const VARS_FILE = "_vars.jsonl";
const TAGS_FILE = "_tags.jsonl";
function openDb(dir: string): DatabaseSync {
mkdirSync(dir, { recursive: true });
const db = new DatabaseSync(join(dir, DB_FILE));
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA foreign_keys = ON");
return db;
}
function initVarTables(db: DatabaseSync): void {
db.exec(`
CREATE TABLE IF NOT EXISTS vars (
name TEXT NOT NULL,
schema TEXT NOT NULL,
value TEXT NOT NULL,
created INTEGER NOT NULL,
updated INTEGER NOT NULL,
tags TEXT NOT NULL DEFAULT '{}',
labels TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY (name, schema)
);
CREATE TABLE IF NOT EXISTS var_history (
name TEXT NOT NULL,
schema TEXT NOT NULL,
value TEXT NOT NULL,
position INTEGER NOT NULL,
set_at INTEGER NOT NULL,
PRIMARY KEY (name, schema, position),
FOREIGN KEY (name, schema) REFERENCES vars(name, schema) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_vars_name ON vars(name);
CREATE INDEX IF NOT EXISTS idx_vars_created ON vars(created);
CREATE INDEX IF NOT EXISTS idx_vars_updated ON vars(updated);
CREATE INDEX IF NOT EXISTS idx_var_history_pos_desc ON var_history(name, schema, position DESC);
`);
}
function initTagTables(db: DatabaseSync): void {
db.exec(`
CREATE TABLE IF NOT EXISTS tags (
target TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT,
created INTEGER NOT NULL,
PRIMARY KEY (target, key)
);
CREATE INDEX IF NOT EXISTS idx_tags_key ON tags(key);
CREATE INDEX IF NOT EXISTS idx_tags_key_value ON tags(key, value);
`);
}
// ── JSONL migration ──
type StoredTag = {
key: string;
value: string | null;
target: Hash;
created: number;
};
function migrateJsonlVars(db: DatabaseSync, dir: string, _cas: CasStore): void {
const path = join(dir, VARS_FILE);
if (!existsSync(path)) return;
const records = new Map<string, VarRecord>();
const byName = new Map<string, Set<string>>();
const content = readFileSync(path, "utf8");
for (const line of content.split("\n")) {
if (line.length === 0) continue;
try {
const rec = JSON.parse(line) as VarRecord & { __op?: string };
if (rec.__op === "remove") {
const k = varKey(rec.name, rec.schema);
records.delete(k);
removeNameIndex(byName, rec.name, k);
} else {
const k = varKey(rec.name, rec.schema);
records.set(k, rec);
addNameIndex(byName, rec.name, k);
}
} catch {
// skip malformed
}
}
if (records.size === 0) return;
const insertVar = db.prepare(`
INSERT OR REPLACE INTO vars (name, schema, value, created, updated, tags, labels)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const insertHistory = db.prepare(`
INSERT OR REPLACE INTO var_history (name, schema, value, position, set_at)
VALUES (?, ?, ?, ?, ?)
`);
transaction(db, () => {
for (const rec of records.values()) {
insertVar.run(
rec.name,
rec.schema,
rec.value,
rec.created,
rec.updated,
JSON.stringify(rec.tags),
JSON.stringify(rec.labels),
);
for (const h of rec.history) {
insertHistory.run(rec.name, rec.schema, h.value, h.position, h.setAt);
}
}
});
}
function migrateJsonlTags(db: DatabaseSync, dir: string): void {
const path = join(dir, TAGS_FILE);
if (!existsSync(path)) return;
const byTarget = new Map<Hash, Map<string, Tag>>();
const content = readFileSync(path, "utf8");
for (const line of content.split("\n")) {
if (line.length === 0) continue;
try {
const ent = JSON.parse(line) as
| (StoredTag & { __op?: "set" | "untag" })
| { __op: "untag"; target: Hash; key: string };
if ((ent as { __op?: string }).__op === "untag") {
const e = ent as { target: Hash; key: string };
const tm = byTarget.get(e.target);
if (tm) {
tm.delete(e.key);
if (tm.size === 0) byTarget.delete(e.target);
}
} else {
const t = ent as StoredTag;
let tm = byTarget.get(t.target);
if (!tm) {
tm = new Map();
byTarget.set(t.target, tm);
}
tm.set(t.key, {
key: t.key,
value: t.value,
target: t.target,
created: t.created,
});
}
} catch {
// skip
}
}
if (byTarget.size === 0) return;
const insertTag = db.prepare(`
INSERT OR REPLACE INTO tags (target, key, value, created)
VALUES (?, ?, ?, ?)
`);
transaction(db, () => {
for (const tm of byTarget.values()) {
for (const tag of tm.values()) {
insertTag.run(tag.target, tag.key, tag.value, tag.created);
}
}
});
}
// ── Row helpers ──
function toVariable(row: Record<string, unknown>): Variable {
return {
name: row.name as string,
schema: row.schema as Hash,
value: row.value as Hash,
created: row.created as number,
updated: row.updated as number,
tags: JSON.parse(row.tags as string) as Record<string, string>,
labels: JSON.parse(row.labels as string) as string[],
};
}
function toHistoryEntry(r: Record<string, unknown>): HistoryEntry {
return {
value: r.value as Hash,
position: r.position as number,
setAt: r.set_at as number,
};
}
function toTag(r: Record<string, unknown>, target: Hash): Tag {
return {
key: r.key as string,
value: r.value as string | null,
target,
created: r.created as number,
};
}
// ── Main factory ──
export function createSqliteVarStore(
dir: string,
cas: CasStore,
): { var: VarStore; tag: TagStore; close: () => void } {
const db = openDb(dir);
initVarTables(db);
initTagTables(db);
// Migrate JSONL if present (one-time, idempotent)
migrateJsonlVars(db, dir, cas);
migrateJsonlTags(db, dir);
let closed = false;
// ── Prepared statements (var) ──
const stmtGetVar = db.prepare(
"SELECT * FROM vars WHERE name = ? AND schema = ?",
);
const stmtGetByName = db.prepare("SELECT * FROM vars WHERE name = ?");
const stmtInsertVar = db.prepare(`
INSERT INTO vars (name, schema, value, created, updated, tags, labels)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const stmtUpdateVar = db.prepare(`
UPDATE vars SET value = ?, updated = ?, tags = ?, labels = ?
WHERE name = ? AND schema = ?
`);
const stmtDeleteVar = db.prepare(
"DELETE FROM vars WHERE name = ? AND schema = ?",
);
const stmtDeleteVarByName = db.prepare("DELETE FROM vars WHERE name = ?");
const stmtInsertHistory = db.prepare(`
INSERT INTO var_history (name, schema, value, position, set_at)
VALUES (?, ?, ?, ?, ?)
`);
const stmtGetHistory = db.prepare(
"SELECT value, position, set_at FROM var_history WHERE name = ? AND schema = ? ORDER BY position DESC",
);
const stmtMaxPosition = db.prepare(
"SELECT MAX(position) as max_pos FROM var_history WHERE name = ? AND schema = ?",
);
const stmtDeleteOldHistory = db.prepare(
"DELETE FROM var_history WHERE name = ? AND schema = ? AND position NOT IN (SELECT position FROM var_history WHERE name = ? AND schema = ? ORDER BY position DESC LIMIT ?)",
);
// ── Prepared statements (tag) ──
const stmtUpsertTag = db.prepare(`
INSERT INTO tags (target, key, value, created) VALUES (?, ?, ?, ?)
ON CONFLICT(target, key) DO UPDATE SET value = excluded.value
`);
const stmtDeleteTag = db.prepare(
"DELETE FROM tags WHERE target = ? AND key = ?",
);
const stmtGetTagsByTarget = db.prepare(
"SELECT * FROM tags WHERE target = ? ORDER BY key",
);
const stmtGetTagsByKey = db.prepare(
"SELECT target, key, value, created FROM tags WHERE key = ? ORDER BY created ASC",
);
const stmtGetTagsByKeyValue = db.prepare(
"SELECT target, key, value, created FROM tags WHERE key = ? AND value = ? ORDER BY created ASC",
);
// ── Transactional helpers ──
function txnSetVar(
name: string,
schema: Hash,
hash: Hash,
now: number,
tagsJson: string,
labelsJson: string,
isNew: boolean,
valueChanged: boolean,
): void {
transaction(db, () => {
if (isNew) {
stmtInsertVar.run(name, schema, hash, now, now, tagsJson, labelsJson);
stmtInsertHistory.run(name, schema, hash, 0, now);
} else if (valueChanged) {
const maxRow = stmtMaxPosition.get(name, schema) as {
max_pos: number | null;
};
const nextPos = (maxRow.max_pos ?? -1) + 1;
stmtInsertHistory.run(name, schema, hash, nextPos, now);
stmtDeleteOldHistory.run(name, schema, name, schema, MAX_HISTORY);
stmtUpdateVar.run(hash, now, tagsJson, labelsJson, name, schema);
} else {
stmtUpdateVar.run(hash, now, tagsJson, labelsJson, name, schema);
}
});
}
function txnTagOps(target: Hash, operations: TagOp[], now: number): void {
transaction(db, () => {
for (const op of operations) {
if (op.op === "set") {
// Use ON CONFLICT to preserve created time — but we need existing created
const existing = db
.prepare("SELECT created FROM tags WHERE target = ? AND key = ?")
.get(target, op.key) as { created: number } | undefined;
const created = existing?.created ?? now;
stmtUpsertTag.run(target, op.key, op.value ?? null, created);
} else {
stmtDeleteTag.run(target, op.key);
}
}
});
}
function txnUntag(target: Hash, keys: string[]): void {
transaction(db, () => {
for (const k of keys) {
stmtDeleteTag.run(target, k);
}
});
}
// ── VarStore implementation ──
const varStore: VarStore = {
set(name: string, hash: Hash, options?: VarSetOptions): Variable {
validateName(name);
const schema = extractSchema(cas, hash);
const existing = stmtGetVar.get(name, schema) as
| Record<string, unknown>
| undefined;
const now = Date.now();
if (existing) {
const v = toVariable(existing);
const tags = options?.tags ?? v.tags;
const labels = options?.labels ?? v.labels;
if (options !== undefined) checkTagLabelConflict(tags, labels);
const valueChanged = v.value !== hash;
const newTags = options !== undefined ? tags : v.tags;
const newLabels = options !== undefined ? labels : v.labels;
if (valueChanged || options !== undefined) {
txnSetVar(
name,
schema,
hash,
valueChanged ? now : v.updated,
JSON.stringify(newTags),
JSON.stringify(newLabels),
false,
valueChanged,
);
}
return {
name,
schema,
value: hash,
created: v.created,
updated: valueChanged ? now : v.updated,
tags: { ...newTags },
labels: [...newLabels],
};
}
// New variable
const tags = options?.tags ?? {};
const labels = options?.labels ?? [];
checkTagLabelConflict(tags, labels);
txnSetVar(
name,
schema,
hash,
now,
JSON.stringify(tags),
JSON.stringify(labels),
true,
false,
);
return {
name,
schema,
value: hash,
created: now,
updated: now,
tags: { ...tags },
labels: [...labels],
};
},
get(name: string, schema?: Hash): Variable | null {
if (schema !== undefined) {
const row = stmtGetVar.get(name, schema) as
| Record<string, unknown>
| undefined;
return row ? toVariable(row) : null;
}
const rows = stmtGetByName.all(name) as Record<string, unknown>[];
if (rows.length !== 1) return null;
return toVariable(rows[0]!);
},
remove(name: string, schema?: Hash): Variable[] {
if (schema !== undefined) {
const row = stmtGetVar.get(name, schema) as
| Record<string, unknown>
| undefined;
if (!row) return [];
const v = toVariable(row);
stmtDeleteVar.run(name, schema);
return [v];
}
const rows = stmtGetByName.all(name) as Record<string, unknown>[];
if (rows.length === 0) return [];
const removed = rows.map(toVariable);
stmtDeleteVarByName.run(name);
return removed;
},
update(name: string, hash: Hash, options?: VarSetOptions): Variable {
validateName(name);
const newSchema = extractSchema(cas, hash);
const rows = stmtGetByName.all(name) as Record<string, unknown>[];
if (rows.length === 0) throw new VariableNotFoundError(name, newSchema);
const existing = stmtGetVar.get(name, newSchema) as
| Record<string, unknown>
| undefined;
if (!existing) {
const first = toVariable(rows[0]!);
throw new SchemaMismatchError(first.schema, newSchema);
}
const v = toVariable(existing);
const now = Date.now();
const tags = options?.tags ?? v.tags;
const labels = options?.labels ?? v.labels;
if (options !== undefined) checkTagLabelConflict(tags, labels);
const valueChanged = v.value !== hash;
const newTags = options !== undefined ? tags : v.tags;
const newLabels = options !== undefined ? labels : v.labels;
if (valueChanged || options !== undefined) {
txnSetVar(
name,
newSchema,
hash,
valueChanged ? now : v.updated,
JSON.stringify(newTags),
JSON.stringify(newLabels),
false,
valueChanged,
);
}
return {
name,
schema: newSchema,
value: hash,
created: v.created,
updated: valueChanged ? now : v.updated,
tags: { ...newTags },
labels: [...newLabels],
};
},
list(options?: VarListOptions): Variable[] {
if (
options?.namePrefix !== undefined &&
options?.exactName !== undefined
) {
throw new Error(
"namePrefix and exactName are mutually exclusive - cannot specify both",
);
}
const limit = options?.limit;
if (limit !== undefined && limit <= 0) return [];
// Build dynamic query
const conditions: string[] = [];
const params: (string | number | null)[] = [];
if (options?.exactName !== undefined) {
conditions.push("name = ?");
params.push(options.exactName);
}
if (options?.namePrefix !== undefined) {
conditions.push("name LIKE ? ESCAPE '\\'");
const escaped = options.namePrefix
.replace(/\\/g, "\\\\")
.replace(/%/g, "\\%")
.replace(/_/g, "\\_");
params.push(`${escaped}%`);
}
if (options?.schema !== undefined) {
conditions.push("schema = ?");
params.push(options.schema);
}
const sortCol = options?.sort === "updated" ? "updated" : "created";
const sortDir = options?.desc ? "DESC" : "ASC";
const where =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
// Post-filter by tags and labels (stored as JSON)
const filterTags = options?.tags ?? {};
const filterLabels = options?.labels ?? [];
const needsPostFilter =
Object.keys(filterTags).length > 0 || filterLabels.length > 0;
// When post-filtering, fetch all matching rows (no SQL LIMIT)
// then apply limit/offset after filtering
let sql: string;
if (needsPostFilter) {
sql = `SELECT * FROM vars ${where} ORDER BY ${sortCol} ${sortDir}, name ASC`;
} else {
sql = `SELECT * FROM vars ${where} ORDER BY ${sortCol} ${sortDir}, name ASC`;
if (limit !== undefined || (options?.offset ?? 0) > 0) {
sql += ` LIMIT ${limit ?? -1} OFFSET ${options?.offset ?? 0}`;
}
}
const rows = db.prepare(sql).all(...params) as Record<string, unknown>[];
if (!needsPostFilter) return rows.map(toVariable);
let results: Variable[] = [];
for (const row of rows) {
const v = toVariable(row);
let ok = true;
for (const [tk, tv] of Object.entries(filterTags)) {
if (v.tags[tk] !== tv) {
ok = false;
break;
}
}
if (!ok) continue;
for (const lb of filterLabels) {
if (!v.labels.includes(lb)) {
ok = false;
break;
}
}
if (ok) results.push(v);
}
// Apply limit/offset after post-filter
const offset = options?.offset ?? 0;
if (offset > 0) results = results.slice(offset);
if (limit !== undefined) results = results.slice(0, limit);
return results;
},
history(name: string, schema?: Hash): HistoryEntry[] {
if (schema !== undefined) {
return (
stmtGetHistory.all(name, schema) as Record<string, unknown>[]
).map(toHistoryEntry);
}
const vars = stmtGetByName.all(name) as Record<string, unknown>[];
if (vars.length !== 1) return [];
const v = vars[0]!;
return (
stmtGetHistory.all(v.name as string, v.schema as string) as Record<
string,
unknown
>[]
).map(toHistoryEntry);
},
close(): void {
if (closed) return;
closed = true;
db.close();
},
};
// ── TagStore implementation ──
const tagStore: TagStore = {
tag(target: Hash, operations: TagOp[]): Tag[] {
const now = Date.now();
txnTagOps(target, operations, now);
return (stmtGetTagsByTarget.all(target) as Record<string, unknown>[]).map(
(r) => toTag(r, target),
);
},
untag(target: Hash, keys: string[]): void {
txnUntag(target, keys);
},
tags(target: Hash): Tag[] {
return (stmtGetTagsByTarget.all(target) as Record<string, unknown>[]).map(
(r) => toTag(r, target),
);
},
listByTag(tag: string, options?: ListOptions): Hash[] {
let key = tag;
let value: string | null | undefined;
const eqIdx = tag.indexOf("=");
if (eqIdx >= 0) {
key = tag.slice(0, eqIdx);
value = tag.slice(eqIdx + 1);
}
// Build SQL with sort/limit/offset pushed down
const sortCol = "created"; // tags only have created
const sortDir = options?.desc ? "DESC" : "ASC";
const offset = options?.offset ?? 0;
const limit = options?.limit;
let sql: string;
const params: (string | number | null)[] = [key];
if (value !== undefined) {
sql = `SELECT target FROM tags WHERE key = ? AND value = ? ORDER BY ${sortCol} ${sortDir}`;
params.push(value);
} else {
sql = `SELECT target FROM tags WHERE key = ? ORDER BY ${sortCol} ${sortDir}`;
}
if (limit !== undefined || offset > 0) {
sql += ` LIMIT ${limit ?? -1} OFFSET ${offset}`;
}
const rows = db.prepare(sql).all(...params) as Record<string, unknown>[];
return rows.map((r) => r.target as Hash);
},
};
return {
var: varStore,
tag: tagStore,
close: () => {
if (closed) return;
closed = true;
db.close();
},
};
}
+1 -1
View File
@@ -1,4 +1,3 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import {
existsSync,
mkdtempSync,
@@ -17,6 +16,7 @@ import {
computeSelfHash,
verify,
} from "@ocas/core";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { createFsStore, openStore } from "./store.js";
+4 -3
View File
@@ -27,7 +27,7 @@ import {
type Store,
} from "@ocas/core";
import { decode } from "cborg";
import { createFsTagStore, createFsVarStoreFor } from "./var-store.js";
import { createSqliteVarStore } from "./sqlite-store.js";
const INDEX_DIR = "_index";
const META_FILE = "_meta";
@@ -393,10 +393,11 @@ export async function prepareStore(dir: string): Promise<FsCasStore> {
*/
export async function openStore(dir: string): Promise<Store> {
const cas = await prepareStore(dir);
const sqlite = createSqliteVarStore(dir, cas);
const ocas: Store = {
cas,
var: createFsVarStoreFor(dir, cas),
tag: createFsTagStore(dir),
var: sqlite.var,
tag: sqlite.tag,
};
bootstrap(ocas);
return ocas;
+6 -15
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { openStore } from "./store.js";
const T1 = "AAAAAAAAAAAAA";
@@ -16,7 +16,7 @@ describe("FsTagStore", () => {
rmSync(dir, { recursive: true, force: true });
});
test("B1. set tag with key/value round-trip + JSONL persisted", async () => {
test("B1. set tag with key/value round-trip + SQLite persisted", async () => {
const store = await openStore(dir);
const result = store.tag.tag(T1, [
{ op: "set", key: "env", value: "prod" },
@@ -26,17 +26,8 @@ describe("FsTagStore", () => {
expect(result[0]?.value).toBe("prod");
expect(store.tag.tags(T1)).toEqual(result);
const jsonl = join(dir, "_tags.jsonl");
expect(existsSync(jsonl)).toBe(true);
const content = readFileSync(jsonl, "utf8");
const lines = content.split("\n").filter((l) => l.length > 0);
expect(lines).toHaveLength(1);
const parsed = JSON.parse(lines[0] as string) as {
key: string;
value: string;
};
expect(parsed.key).toBe("env");
expect(parsed.value).toBe("prod");
const dbFile = join(dir, "_store.db");
expect(existsSync(dbFile)).toBe(true);
});
test("B2. label tag (no value) records value: null", async () => {
@@ -120,7 +111,7 @@ describe("FsTagStore", () => {
expect(tags.map((t) => t.value)).toEqual(["prod", "platform"]);
});
test("B11. JSONL replay fidelity (set/delete/untag mix)", async () => {
test("B11. SQLite replay fidelity (set/delete/untag mix)", async () => {
const store = await openStore(dir);
store.tag.tag(T1, [{ op: "set", key: "a", value: "1" }]);
store.tag.tag(T1, [{ op: "set", key: "b", value: "2" }]);
+5 -14
View File
@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Hash, Store } from "@ocas/core";
@@ -11,6 +10,7 @@ import {
TagLabelConflictError,
VariableNotFoundError,
} from "@ocas/core";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { openStore } from "./store.js";
const META_TYPE_KEY = Symbol.for("@ocas/core/bootstrap-store");
@@ -40,7 +40,7 @@ describe("FsVarStore", () => {
rmSync(dir, { recursive: true, force: true });
});
test("A1. set + get round-trip persists to JSONL", async () => {
test("A1. set + get round-trip persists to SQLite", async () => {
const { store, schema, put } = await setupStore(dir);
const h = put("hello");
const v = store.var.set("@app/x", h);
@@ -51,17 +51,8 @@ describe("FsVarStore", () => {
const got = store.var.get("@app/x", schema);
expect(got?.value).toBe(h);
const jsonl = join(dir, "_vars.jsonl");
expect(existsSync(jsonl)).toBe(true);
const content = readFileSync(jsonl, "utf8");
expect(content.length).toBeGreaterThan(0);
const lines = content.split("\n").filter((l) => l.length > 0);
expect(lines.length).toBeGreaterThanOrEqual(1);
const matching = lines
.map((l) => JSON.parse(l) as { name?: string; value?: Hash })
.find((r) => r.name === "@app/x");
expect(matching).toBeDefined();
expect(matching?.value).toBe(h);
const dbFile = join(dir, "_store.db");
expect(existsSync(dbFile)).toBe(true);
});
test("A2. name validation", async () => {
-418
View File
@@ -1,418 +0,0 @@
import {
appendFileSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import type {
CasStore,
Hash,
ListEntry,
Tag,
TagStore,
Variable,
VarListOptions,
VarStore,
} from "@ocas/core";
import {
addNameIndex,
applyListOptions,
casListEntry,
checkTagLabelConflict,
cloneVarRecord,
extractSchema,
pushHistory,
removeNameIndex,
SchemaMismatchError,
VariableNotFoundError,
type VarRecord,
validateName,
varKey,
} from "@ocas/core";
const VARS_FILE = "_vars.jsonl";
const TAGS_FILE = "_tags.jsonl";
export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
const records = new Map<string, VarRecord>();
const byName = new Map<string, Set<string>>();
const path = join(dir, VARS_FILE);
// Load existing records (last record per key wins)
try {
const content = readFileSync(path, "utf8");
for (const line of content.split("\n")) {
if (line.length === 0) continue;
try {
const rec = JSON.parse(line) as VarRecord & { __op?: string };
if (rec.__op === "remove") {
const k = varKey(rec.name, rec.schema);
records.delete(k);
removeNameIndex(byName, rec.name, k);
} else {
const k = varKey(rec.name, rec.schema);
records.set(k, rec);
addNameIndex(byName, rec.name, k);
}
} catch {
// skip malformed
}
}
} catch {
// file may not exist
}
function persistFull(): void {
mkdirSync(dir, { recursive: true });
const lines: string[] = [];
for (const rec of records.values()) {
lines.push(JSON.stringify(rec));
}
writeFileSync(path, lines.length ? `${lines.join("\n")}\n` : "", "utf8");
}
function appendRecord(rec: VarRecord): void {
mkdirSync(dir, { recursive: true });
appendFileSync(path, `${JSON.stringify(rec)}\n`, "utf8");
}
function appendRemoval(name: string, schema: Hash): void {
mkdirSync(dir, { recursive: true });
appendFileSync(
path,
`${JSON.stringify({ __op: "remove", name, schema })}\n`,
"utf8",
);
}
return {
set(name, hash, options) {
validateName(name);
const schema = extractSchema(cas, hash);
const k = varKey(name, schema);
const existing = records.get(k);
const now = Date.now();
if (existing) {
const tags = options?.tags ?? existing.tags;
const labels = options?.labels ?? existing.labels;
if (options !== undefined) checkTagLabelConflict(tags, labels);
const changed = pushHistory(existing, hash, now);
if (changed) {
existing.value = hash;
existing.updated = now;
}
if (options !== undefined) {
existing.tags = { ...tags };
existing.labels = [...labels];
}
persistFull();
return cloneVarRecord(existing);
}
const tags = options?.tags ?? {};
const labels = options?.labels ?? [];
checkTagLabelConflict(tags, labels);
const rec: VarRecord = {
name,
schema,
value: hash,
created: now,
updated: now,
tags: { ...tags },
labels: [...labels],
history: [{ value: hash, position: 0, setAt: now }],
};
records.set(k, rec);
addNameIndex(byName, name, k);
appendRecord(rec);
return cloneVarRecord(rec);
},
get(name, schema) {
if (schema !== undefined) {
const rec = records.get(varKey(name, schema));
return rec ? cloneVarRecord(rec) : null;
}
const set = byName.get(name);
if (!set || set.size !== 1) return null;
const onlyKey = set.values().next().value;
if (onlyKey === undefined) return null;
const rec = records.get(onlyKey);
return rec ? cloneVarRecord(rec) : null;
},
remove(name, schema) {
if (schema !== undefined) {
const k = varKey(name, schema);
const rec = records.get(k);
if (!rec) return [];
records.delete(k);
removeNameIndex(byName, name, k);
appendRemoval(name, schema);
return [cloneVarRecord(rec)];
}
const set = byName.get(name);
if (!set) return [];
const removed: Variable[] = [];
for (const k of [...set]) {
const rec = records.get(k);
if (rec) {
removed.push(cloneVarRecord(rec));
records.delete(k);
appendRemoval(rec.name, rec.schema);
}
}
byName.delete(name);
return removed;
},
update(name, hash, options) {
validateName(name);
const newSchema = extractSchema(cas, hash);
const set = byName.get(name);
if (!set || set.size === 0)
throw new VariableNotFoundError(name, newSchema);
const k = varKey(name, newSchema);
const existing = records.get(k);
if (!existing) {
for (const ek of set) {
const erec = records.get(ek);
if (erec) throw new SchemaMismatchError(erec.schema, newSchema);
}
throw new VariableNotFoundError(name, newSchema);
}
const now = Date.now();
const tags = options?.tags ?? existing.tags;
const labels = options?.labels ?? existing.labels;
if (options !== undefined) checkTagLabelConflict(tags, labels);
const changed = pushHistory(existing, hash, now);
if (changed) {
existing.value = hash;
existing.updated = now;
}
if (options !== undefined) {
existing.tags = { ...tags };
existing.labels = [...labels];
}
persistFull();
return cloneVarRecord(existing);
},
list(options?: VarListOptions) {
if (
options?.namePrefix !== undefined &&
options?.exactName !== undefined
) {
throw new Error(
"namePrefix and exactName are mutually exclusive - cannot specify both",
);
}
const namePrefix = options?.namePrefix;
const exactName = options?.exactName;
const schema = options?.schema;
const filterTags = options?.tags ?? {};
const filterLabels = options?.labels ?? [];
const sort = options?.sort ?? "created";
const desc = options?.desc ?? false;
const limit = options?.limit;
const offset = options?.offset ?? 0;
if (limit !== undefined && limit <= 0) return [];
let results: VarRecord[] = [];
for (const rec of records.values()) {
if (exactName !== undefined && rec.name !== exactName) continue;
if (namePrefix !== undefined && !rec.name.startsWith(namePrefix))
continue;
if (schema !== undefined && rec.schema !== schema) continue;
let ok = true;
for (const [tk, tv] of Object.entries(filterTags)) {
if (rec.tags[tk] !== tv) {
ok = false;
break;
}
}
if (!ok) continue;
for (const lb of filterLabels) {
if (!rec.labels.includes(lb)) {
ok = false;
break;
}
}
if (!ok) continue;
results.push(rec);
}
results.sort((a, b) => {
const av = sort === "updated" ? a.updated : a.created;
const bv = sort === "updated" ? b.updated : b.created;
if (av !== bv) return desc ? bv - av : av - bv;
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
if (offset > 0) results = results.slice(offset);
if (limit !== undefined) results = results.slice(0, limit);
return results.map(cloneVarRecord);
},
history(name, schema) {
if (schema !== undefined) {
const rec = records.get(varKey(name, schema));
return rec ? rec.history.map((e) => ({ ...e })) : [];
}
const set = byName.get(name);
if (!set || set.size !== 1) return [];
const onlyKey = set.values().next().value;
if (onlyKey === undefined) return [];
const rec = records.get(onlyKey);
return rec ? rec.history.map((e) => ({ ...e })) : [];
},
close() {
// no-op (synchronous file ops)
},
};
}
type StoredTag = {
key: string;
value: string | null;
target: Hash;
created: number;
};
export function createFsTagStore(dir: string): TagStore {
const byTarget = new Map<Hash, Map<string, Tag>>();
const byKey = new Map<string, Set<Hash>>();
const path = join(dir, TAGS_FILE);
function addKeyIndex(k: string, target: Hash): void {
let set = byKey.get(k);
if (!set) {
set = new Set();
byKey.set(k, set);
}
set.add(target);
}
function removeKeyIndex(k: string, target: Hash): void {
const set = byKey.get(k);
if (!set) return;
const tmap = byTarget.get(target);
if (tmap?.has(k)) return;
set.delete(target);
if (set.size === 0) byKey.delete(k);
}
// Load
try {
const content = readFileSync(path, "utf8");
for (const line of content.split("\n")) {
if (line.length === 0) continue;
try {
const ent = JSON.parse(line) as
| (StoredTag & { __op?: "set" | "untag" })
| { __op: "untag"; target: Hash; key: string };
if ((ent as { __op?: string }).__op === "untag") {
const e = ent as { target: Hash; key: string };
const tm = byTarget.get(e.target);
if (tm) {
tm.delete(e.key);
removeKeyIndex(e.key, e.target);
if (tm.size === 0) byTarget.delete(e.target);
}
} else {
const t = ent as StoredTag;
let tm = byTarget.get(t.target);
if (!tm) {
tm = new Map();
byTarget.set(t.target, tm);
}
tm.set(t.key, {
key: t.key,
value: t.value,
target: t.target,
created: t.created,
});
addKeyIndex(t.key, t.target);
}
} catch {
// skip
}
}
} catch {
// none
}
function append(line: object): void {
mkdirSync(dir, { recursive: true });
appendFileSync(path, `${JSON.stringify(line)}\n`, "utf8");
}
return {
tag(target, ops) {
let tm = byTarget.get(target);
if (!tm) {
tm = new Map();
byTarget.set(target, tm);
}
const now = Date.now();
for (const op of ops) {
if (op.op === "set") {
const existing = tm.get(op.key);
const tag: Tag = {
key: op.key,
value: op.value ?? null,
target,
created: existing?.created ?? now,
};
tm.set(op.key, tag);
addKeyIndex(op.key, target);
append(tag);
} else {
tm.delete(op.key);
removeKeyIndex(op.key, target);
append({ __op: "untag", target, key: op.key });
}
}
return [...tm.values()].sort((a, b) =>
a.key < b.key ? -1 : a.key > b.key ? 1 : 0,
);
},
untag(target, keys) {
const tm = byTarget.get(target);
if (!tm) return;
for (const k of keys) {
tm.delete(k);
removeKeyIndex(k, target);
append({ __op: "untag", target, key: k });
}
if (tm.size === 0) byTarget.delete(target);
},
tags(target) {
const tm = byTarget.get(target);
if (!tm) return [];
return [...tm.values()].sort((a, b) =>
a.key < b.key ? -1 : a.key > b.key ? 1 : 0,
);
},
listByTag(tag, options) {
let key = tag;
let value: string | null | undefined;
const eqIdx = tag.indexOf("=");
if (eqIdx >= 0) {
key = tag.slice(0, eqIdx);
value = tag.slice(eqIdx + 1);
}
const targets = byKey.get(key);
if (!targets) return [];
let entries: ListEntry[] = [];
for (const t of targets) {
const tm = byTarget.get(t);
if (!tm) continue;
const tagEntry = tm.get(key);
if (!tagEntry) continue;
if (value !== undefined && tagEntry.value !== value) continue;
entries.push(casListEntry(t, tagEntry.created));
}
entries = applyListOptions(entries, options);
return entries.map((e) => e.hash);
},
};
}
+2261
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
packages:
- "packages/*"
minimumReleaseAge: 0
+18
View File
@@ -0,0 +1,18 @@
name: "@ocas/workspace"
runtime: node
packages:
- name: "@ocas/core"
path: packages/core
type: lib
- name: "@ocas/fs"
path: packages/fs
type: lib
- name: "@ocas/cli"
path: packages/cli
type: cli
changeset:
fixed: true
release:
registry: https://registry.npmjs.org
access: public
gitTagPrefix: v
-8
View File
@@ -1,8 +0,0 @@
#!/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
-123
View File
@@ -1,123 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# OCAS Release Preparation
# Creates a release branch, runs changeset version, and validates.
#
# Usage: ./scripts/prepare-release.sh
#
# Prerequisites:
# - Clean working tree on main branch
# - Pending changesets in .changeset/
BOLD='\033[1m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
info() { echo -e "${BOLD}${GREEN}${NC} $1"; }
warn() { echo -e "${BOLD}${YELLOW}${NC} $1"; }
error() { echo -e "${BOLD}${RED}${NC} $1"; exit 1; }
# --- Pre-flight checks ---
echo -e "\n${BOLD}OCAS Release Preparation${NC}\n"
# Must be on main
BRANCH=$(git branch --show-current)
[[ "$BRANCH" == "main" ]] || error "Must be on main branch (currently on $BRANCH)"
# Clean working tree
[[ -z "$(git status --porcelain)" ]] || error "Working tree is not clean. Commit or stash changes first."
# Check for pending changesets
CHANGESETS=$(ls .changeset/*.md 2>/dev/null | grep -v README.md || true)
if [[ -z "$CHANGESETS" ]]; then
error "No pending changesets found. Run 'bunx changeset' to add one first."
fi
info "Found pending changesets:"
for cs in $CHANGESETS; do
echo " $(basename "$cs")"
done
# --- Determine version ---
# Dry-run to peek at the version bump
echo ""
info "Previewing version changes..."
bunx changeset status
# --- Create release branch ---
# Get current version to name the branch
CURRENT_VERSION=$(python3 -c "import json; print(json.load(open('packages/core/package.json'))['version'])")
info "Current version: $CURRENT_VERSION"
git fetch origin
git checkout -b release/next
# --- Run changeset version ---
info "Running changeset version..."
bunx changeset version
# Show what changed
NEW_VERSION=$(python3 -c "import json; print(json.load(open('packages/core/package.json'))['version'])")
info "New version: $NEW_VERSION"
# Rename branch to include actual 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 ""
info "Running validation..."
echo " → bun install"
bun install --no-cache
echo " → bun run build"
bun run build
echo " → bun run check"
bun run check || warn "Lint warnings found (review above)"
echo " → bun test"
bun test || error "Tests failed!"
info "All checks passed"
# --- Commit ---
git add -A
git commit -m "chore(release): prepare v$NEW_VERSION"
echo ""
info "Release branch ready: release/$NEW_VERSION"
echo ""
echo " Next steps:"
echo " 1. Review changes: git diff main...HEAD"
echo " 2. Fix issues if needed, commit to this branch"
echo " 3. Prerelease: ./scripts/publish.sh --rc"
echo " 4. Final release: ./scripts/publish.sh"
echo ""
-190
View File
@@ -1,190 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# OCAS Publish
# Builds, publishes to npm, tags, and pushes.
#
# 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)
# - npm authenticated (`npm whoami` works)
# - All checks passing
BOLD='\033[1m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
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"
# Must be on release/* branch
BRANCH=$(git branch --show-current)
[[ "$BRANCH" == release/* ]] || error "Must be on a release/* branch (currently on $BRANCH)"
# Clean working tree
[[ -z "$(git status --porcelain)" ]] || error "Working tree is not clean. Commit changes first."
# Extract base version from package.json
BASE_VERSION=$(python3 -c "import json; print(json.load(open('packages/core/package.json'))['version'])")
# 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
npm whoami &>/dev/null || error "Not authenticated with npm. Run 'npm login' first."
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..."
echo " → bun run build"
bun run build
echo " → bun test"
bun test || error "Tests failed! Fix before publishing."
# --- 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 (tag: $NPM_TAG)"
done
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 --tag "$NPM_TAG")
info "$PKG_NAME@$VERSION published ✓"
done
# --- Commit version changes ---
git add -A
if [[ -n "$(git diff --cached --name-only)" ]]; then
git commit -m "chore(release): set version $VERSION"
fi
# --- For final release: tag, push, merge back ---
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"
# Merge back to main
echo ""
info "Merging release into main..."
git checkout main
git merge "$BRANCH" --no-ff -m "chore: merge release $TAG"
# 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 ""
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"types": ["bun-types"],
"types": ["node"],
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["packages/*/src/**/*.test.ts", "packages/*/tests/**/*.test.ts"],
testTimeout: 30000,
},
});