Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 88 additions & 104 deletions .github/workflows/web-demos.yml
Original file line number Diff line number Diff line change
@@ -1,110 +1,94 @@
name: Coln Lab

on:
pull_request:
push:
branches: [main]

name: Web demos

permissions:
contents: read
pull-requests: write
branches: [live-demos]

concurrency:
group: web-demos-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: pages
cancel-in-progress: false

jobs:
# build-and-deploy:
# runs-on: ubuntu-latest
# env:
# CARGO_TARGET_DIR: ${{ github.workspace }}/target
# NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
# NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

# steps:
# - name: Checkout ⬇️
# uses: actions/checkout@v6

# - name: Install Nix ❄️
# uses: cachix/install-nix-action@v31

# - name: Set up Cachix ♻️
# if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
# uses: cachix/cachix-action@v17
# with:
# name: coln
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

# - name: Restore build caches
# uses: actions/cache@v4
# with:
# path: |
# .pnpm-store
# ~/.npm
# ~/.cargo/registry
# ~/.cargo/git
# ~/.ghc-wasm/.cabal
# target
# dist-newstyle
# key: web-demos-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'examples/sync-demo/pnpm-lock.yaml') }}
# restore-keys: |
# web-demos-${{ runner.os }}-

# - name: Build sync demo
# run: nix run --accept-flake-config .#sync-demo -- "$GITHUB_WORKSPACE"
# env:
# VITE_BASE: /sync/

# - name: Build web compiler
# run: |
# nix develop --accept-flake-config --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z'
# nix develop --accept-flake-config --command just examples/build-web-compiler

# - name: Assemble deploy directory
# run: |
# cp examples/index.html examples/style.css _build/web/
# cp -r examples/sync-demo/dist _build/web/sync

# - name: Deploy PR preview to Netlify
# id: deploy-preview
# if: github.event_name == 'pull_request' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != ''
# run: |
# npx --yes netlify-cli@latest deploy \
# --auth "$NETLIFY_AUTH_TOKEN" \
# --site "$NETLIFY_SITE_ID" \
# --dir _build/web \
# --no-build \
# --alias "pr-${{ github.event.pull_request.number }}" \
# --message "PR #${{ github.event.pull_request.number }} web demos" \
# --json > netlify-deploy.json

# cat netlify-deploy.json
# deploy_url=$(node -e "const d = require('./netlify-deploy.json'); console.log(d.deploy_ssl_url || d.ssl_url || d.deploy_url || d.url)")
# echo "deploy-url=$deploy_url" >> "$GITHUB_OUTPUT"

# - name: Comment with deploy preview URL
# if: github.event_name == 'pull_request' && steps.deploy-preview.outputs.deploy-url != ''
# uses: actions/github-script@v8
# with:
# script: |
# const marker = '<!-- coln-web-demos-deploy-preview -->'
# const body = `${marker}\nWeb demo preview: ${{ steps.deploy-preview.outputs.deploy-url }}`
# const { owner, repo } = context.repo
# const issue_number = context.issue.number
# const comments = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 })
# const existing = comments.data.find(comment => comment.body?.includes(marker))
# if (existing) {
# await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body })
# } else {
# await github.rest.issues.createComment({ owner, repo, issue_number, body })
# }
# - name: Deploy main to Netlify production
# if: github.event_name == 'push' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != ''
# run: |
# npx --yes netlify-cli@latest deploy \
# --auth "$NETLIFY_AUTH_TOKEN" \
# --site "$NETLIFY_SITE_ID" \
# --dir _build/web \
# --no-build \
# --prod \
# --message "web demos ${{ github.sha }}"
build:
runs-on: ubuntu-latest
permissions:
contents: read
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
PNPM_STORE_DIR: ${{ github.workspace }}/.pnpm-store
VITE_BASE: /coln/
VITE_SUBDUCTION_ENDPOINT: wss://coln.sync.inkandswitch.com

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install Nix
uses: cachix/install-nix-action@v31

- name: Set up Cachix
if: ${{ env.CACHIX_AUTH_TOKEN != '' }}
uses: cachix/cachix-action@v17
with:
name: coln
authToken: ${{ env.CACHIX_AUTH_TOKEN }}

- name: Restore build caches
uses: actions/cache@v4
with:
path: |
.pnpm-store
~/.npm
~/.cargo/registry
~/.cargo/git
~/.ghc-wasm/.cabal
target
dist-newstyle
key: lab-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'packages/coln-repo/pnpm-lock.yaml', 'examples/lab/pnpm-lock.yaml') }}
restore-keys: |
lab-${{ runner.os }}-

- name: Build linked packages
run: |
nix develop --accept-flake-config --command npm ci --prefix packages/coln-js-runtime
nix develop --accept-flake-config --command npm run --prefix packages/coln-js-runtime build
nix develop --accept-flake-config --command pnpm --dir packages/coln-repo install --frozen-lockfile --store-dir "$PNPM_STORE_DIR"
nix develop --accept-flake-config --command pnpm --dir packages/coln-repo build

- name: Install Lab dependencies
run: nix develop --accept-flake-config --command pnpm --dir examples/lab install --frozen-lockfile --store-dir "$PNPM_STORE_DIR"

- name: Build compiler WASM
run: |
nix develop --accept-flake-config --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z'
nix develop --accept-flake-config --command just examples/stage-web-lab

- name: Test Lab
run: nix develop --accept-flake-config --command pnpm --dir examples/lab test

- name: Build Lab
run: nix develop --accept-flake-config --command pnpm --dir examples/lab build

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: _build/web/lab-app

deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ TSWLatexianTemp*
.shake

_build/
.env
.env.local
/cabal.project.local

# nix result
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions examples/cli-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
node_modules/
test-results/
playwright-report/
139 changes: 139 additions & 0 deletions examples/cli-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Coln CLI Demo

`coln-repo` is an experimental, JSON-only CLI for agents to read and update an
existing synchronized Coln document. It exposes the document's compiled IR
without interpreting its domain and evaluates trusted, synchronous JavaScript
for reads and transactions.

Nothing in this protocol is stable yet.

## Build

Build local packages first:

```bash
npm ci --prefix packages/coln-js-runtime
npm run --prefix packages/coln-js-runtime build
pnpm --dir packages/coln-repo install
pnpm --dir packages/coln-repo build
pnpm --dir examples/cli-demo install
pnpm --dir examples/cli-demo build
```

The built binary is `examples/cli-demo/dist/cli.js`, exposed as `coln-repo` by
the package manifest. Put it on your `PATH` with:

```bash
pnpm --dir examples/cli-demo link --global
```

The examples below assume `coln-repo` is on `PATH`; otherwise substitute
`node examples/cli-demo/dist/cli.js`.

## Agent Skill

The model-facing workflow lives in [`skills/coln-repo/SKILL.md`](./skills/coln-repo/SKILL.md),
a portable skill (frontmatter plus markdown) that OpenCode, Claude Code, and
similar harnesses can load. Two ways to reach it:

```bash
coln-repo guide # print the skill body as JSON
coln-repo install-skill # copy to ~/.agents/skills/coln-repo
coln-repo install-skill --dir ~/.claude/skills
```

`install-skill` reports `installed`, `updated`, or `unchanged`; rerun it after
upgrading the CLI to refresh the installed copy. No harness configuration or
permission rules are shipped; see Security below.

## Commands

Every command takes the document as a named flag. `--endpoint` overrides
`SUBDUCTION_ENDPOINT`; otherwise the public Subduction relay is used.

```bash
coln-repo ir --document <automerge-url> [--endpoint <ws-url>]
```

`ir` returns the compiled JSON IR unchanged inside the response's `ir` field.
Agents should use it as structural evidence, not domain documentation.

`query` reads one JavaScript expression from stdin. A read-only `store` is in
scope with `jsonIR()`, `scanTable(path)`, `rowById(path, rowRef)`, and `heads()`.

```bash
coln-repo query --document automerge:... <<'JS'
store.scanTable("Records.Documents")
JS
```

`exec` reads a JavaScript function body from stdin. The current transaction API
is in scope as `txn`; today it exposes `txn.add(path, values)`. Return a
JSON-serializable value to include it in the response.

```bash
coln-repo exec --document automerge:... <<'JS'
const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }])
const document = txn.add("Records.Documents", [
folder,
{ tag: "string", value: "Notes" },
])
return { folder, document }
JS
```

Values passed to `txn.add` are tagged:

```js
{ tag: "string", value: "hello" }
{ tag: "int", value: 42 }
{ tag: "row_id", value: rowReference }
```

`txn.add` returns a complete tagged `row_id` value. Pass that value directly to
later `txn.add` calls; do not wrap it in another `{ tag: "row_id", ... }`.

Programs must complete synchronously. Their return values must be JSON
serializable. Program `console` output is redirected to stderr so stdout remains
one JSON response.

## Responses

Every response contains `protocolVersion: 0`, `ok`, and the command where known.
Successful writes include heads before and after the local commit plus sync
status. Errors identify their phase and whether a mutation was applied locally.

| Exit | Code | Phase | Mutation state |
|---|---|---|---|
| 2 | `INVALID_ARGUMENTS`, `PROGRAM_REQUIRED` | `arguments` | `not_applied` |
| 3 | `DOCUMENT_OPEN_FAILED` | `open` | `not_applied` |
| 4 | `IR_FAILED`, `QUERY_FAILED` | `read`, `evaluate` | `not_applied` |
| 4 | `GUIDE_FAILED`, `SKILL_INSTALL_FAILED` | `read`, `install` | `not_applied` |
| 5 | `TRANSACTION_FAILED` | `transaction` | `not_applied` |
| 5 | `RESULT_INVALID` | `result` | `not_applied` |
| 5 | `RESULT_FAILED` | `result` | `applied_locally` |
| 6 | `SYNC_FAILED` | `flush` | `applied_locally` |
| 1 | `INTERNAL_ERROR` | `internal` | `unknown` |

`RESULT_INVALID` means the program's return value was not JSON; the transaction
was rolled back so the caller never loses the row references it asked for.

If an `exec` response reports `mutationState: "applied_locally"`, do not retry it
automatically: the local commit succeeded, but relay synchronization or result
reporting failed afterwards. The CLI does not judge retryability of
`not_applied` failures; connection timeouts may be transient, schema rejections
are not.

## Security

`query` and `exec` evaluate trusted JavaScript with this Node process's
capabilities. Agent harnesses should request approval for them by default.
Allowing either command without approval is equivalent to allowing arbitrary
local Node execution. `ir`, `guide`, and `install-skill` do not evaluate user
JavaScript and may be allowlisted separately.

## Verify

```bash
pnpm --dir examples/cli-demo test
```
33 changes: 33 additions & 0 deletions examples/cli-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "coln-cli-demo",
"private": true,
"type": "module",
"license": "(Apache-2.0 OR MIT)",
"bin": {
"coln-repo": "./dist/cli.js"
},
"files": [
"dist",
"skills"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"cli": "tsx src/cli.ts",
"typecheck": "tsc --noEmit",
"test": "pnpm run typecheck && pnpm run test:e2e",
"test:e2e": "pnpm run build && playwright test"
},
"dependencies": {
"@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo",
"@coln-project/repo": "link:../../packages/coln-repo"
},
"devDependencies": {
"@automerge/automerge-subduction": "0.16.1",
"@playwright/test": "^1.62.1",
"@types/node": "^25.9.5",
"@types/ws": "^8.18.1",
"tsx": "^4.23.13",
"typescript": "^6.0.3",
"ws": "^8.21.3"
}
}
Loading