Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: tests/refresh-fixtures.sh
- name: Fail if the vendored copies drifted
run: |
git diff --exit-code -- tests/fixtures || {
echo "::error::A hook contract moved in lich. Run tests/refresh-fixtures.sh, then make the scripts match."
git diff --exit-code -- tests/fixtures skills/theme/template.json || {
echo "::error::A hook contract or the theme template moved in lich. Run tests/refresh-fixtures.sh, then make the scripts (or the theme skill) match."
exit 1
}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **The theme validator knows the status tones.** `validate.mjs` read its
tokens off a template copied before lich 0.53.0 made `tone-pass` and
`tone-wait` required, so it called a current theme's tones unknown and passed
a theme without them that lich rejects on import. The template is now
refreshed from lich at `tests/lich-ref` with the contract fixtures, and CI
fails when the copy drifts; the skill lists the two tokens.

## [0.18.1] - 2026-10-08

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Each doc carries the event mapping for **every** provider, one column each — a

## Skills

- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too.
- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, vendored at `tests/lich-ref` by `tests/refresh-fixtures.sh` and diffed by CI like the fixtures, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too.

## Tests

Expand Down
3 changes: 2 additions & 1 deletion skills/theme/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ manifest**, commit, push. The user takes it with **Update** on the theme's row.
`com1`–`com9`, `lpt1`–`lpt9`) — the id names a file.
- `name`: required, non-blank, ≤ 128 characters.
- `scheme`: `light` or `dark`. Nothing else.
- `app`: **every one of the 31 tokens** must be present. An unknown token is an
- `app`: **every one of the 33 tokens** must be present. An unknown token is an
error, not a warning.
- `terminal`: `background` and `foreground` required; the other 20 tokens
optional. Unknown tokens rejected.
Expand Down Expand Up @@ -199,6 +199,7 @@ pair contrasty, they are never used apart):
| `muted` / `muted-foreground` | secondary text, resting icons, paths, meta |
| `primary` / `primary-foreground` | the single high-emphasis button fill, and the Switch when on |
| `destructive` | destructive actions and diff removals — must stay legible as red |
| `tone-pass` / `tone-wait` | status tones: a pass (green) and a wait (amber) on session icons, pull request checks and reviews. Drawn as text and icons on `background`/`card`, so they need contrast there. Required since lich 0.53.0 — a pack that carries them is one an older lich rejects as unknown tokens, which is what `minLichVersion: "0.53.0"` names |
| `border` | hairline seams. Translucent works well in dark (`oklch(1 0 0 / 10%)`) |
| `input` | input and control edges, usually a touch stronger than `border` |
| `ring` | focus ring, always visible |
Expand Down
3 changes: 3 additions & 0 deletions skills/theme/template.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/omartelo/lich/main/themes/lich-theme.schema.json",
"id": "purple-night",
"name": "Purple Night",
"scheme": "dark",
Expand All @@ -18,6 +19,8 @@
"accent": "#3a2d57",
"accent-foreground": "#f4efff",
"destructive": "#ff5c8a",
"tone-pass": "oklch(0.696 0.17 162.48)",
"tone-wait": "oklch(0.769 0.188 70.08)",
"border": "#4a3a68",
"input": "#554178",
"ring": "#c4a7ff",
Expand Down
10 changes: 8 additions & 2 deletions tests/refresh-fixtures.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/sh
# Refreshes the vendored copies of lich's hook-contract fixtures.
# Refreshes the vendored copies of lich's hook-contract fixtures, and of the
# theme template the theme skill hands out and validates against.
#
# The fixtures are canonical in the lich repository (docs/hooks/fixtures/
# there). This repository only vendors them so the test suite never needs the
Expand All @@ -15,7 +16,8 @@ set -eu

here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ref=$(tr -d '[:space:]' <"$here/lich-ref")
base=https://raw.githubusercontent.com/omartelo/lich/$ref/docs/hooks/fixtures
raw=https://raw.githubusercontent.com/omartelo/lich/$ref
base=$raw/docs/hooks/fixtures

for file in session-state.jsonl session-start.jsonl session-title.jsonl \
session-touched.jsonl mod-usage.jsonl mod-control.jsonl mod-commands.json \
Expand All @@ -24,3 +26,7 @@ for file in session-state.jsonl session-start.jsonl session-title.jsonl \
echo "refreshed $file at $ref"
done

# validate.mjs reads its token sets off this copy, so a token lich adds reaches
# the validator by moving tests/lich-ref, not by hand.
curl -fsSL "$raw/themes/template.json" -o "$here/../skills/theme/template.json"
echo "refreshed skills/theme/template.json at $ref"
41 changes: 41 additions & 0 deletions tests/theme-validate.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Pins skills/theme/validate.mjs against the app tokens lich requires on
// import: a theme it calls ok must be one lich installs, and the other way round.
//
// Run: node --test tests/

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

const validator = new URL('../skills/theme/validate.mjs', import.meta.url).pathname
const template = JSON.parse(
readFileSync(new URL('../skills/theme/template.json', import.meta.url), 'utf8'),
)

// The status tones lich 0.53.0 made required (omartelo/lich#617), with the
// bundled Dark theme's values.
const tones = {
'tone-pass': 'oklch(0.696 0.17 162.48)',
'tone-wait': 'oklch(0.769 0.188 70.08)',
}

function validate(app) {
const path = join(mkdtempSync(join(tmpdir(), 'lich-theme-')), 'theme.json')
writeFileSync(path, JSON.stringify({ ...template, id: 'tones', app }))
return spawnSync(process.execPath, [validator, path], { encoding: 'utf8' })
}

test('a theme carrying the status tones passes', () => {
const run = validate({ ...template.app, ...tones })
assert.equal(run.status, 0, run.stderr)
})

test('a theme missing a status tone is rejected the way lich rejects it', () => {
const { 'tone-wait': _, ...app } = { ...template.app, ...tones }
const run = validate(app)
assert.equal(run.status, 1)
assert.match(run.stderr, /app\.tone-wait is required/)
})
Loading