@lemoncode/lemony 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/catalog/VERSION +1 -1
- package/catalog/commands/sync-design-tokens.md +3 -1
- package/catalog/hooks/init.sh +23 -1
- package/catalog/hooks/lib/merge-pr.sh +3 -2
- package/catalog/hooks/lib/playbook-scan.sh +4 -2
- package/catalog/skills/design-tool-sync/SKILL.md +40 -7
- package/catalog/templates/claude-code/harness.config.yml.tpl +5 -2
- package/dist/cli.mjs +920 -297
- package/package.json +1 -1
package/catalog/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.5.
|
|
1
|
+
0.5.1
|
|
@@ -18,7 +18,9 @@ Designer; follow the steps there.
|
|
|
18
18
|
deletes tool-only variables). Preview the plan, push on confirmation, then record the drift
|
|
19
19
|
baseline only after the push succeeds.
|
|
20
20
|
- _empty_ — check the drift state (`lemony status`) and, if an export is pending and the tool
|
|
21
|
-
is connected, offer `export`; otherwise report the current state.
|
|
21
|
+
is connected, offer `export`; otherwise report the current state. `status` shows no drift
|
|
22
|
+
line both when there is no token file and when the file cannot be read or fails
|
|
23
|
+
validation — tell the two apart with `lemony doctor`'s `design-tool-drift` check.
|
|
22
24
|
|
|
23
25
|
The design tool is a **projection** of the canonical JSON, never a peer source of truth.
|
|
24
26
|
Detect the tool at runtime: read the `com.lemony.design-tool` binding at the root of
|
package/catalog/hooks/init.sh
CHANGED
|
@@ -46,8 +46,17 @@ WARNINGS=()
|
|
|
46
46
|
# awk/grep/git (preinstalled) cover the rest.
|
|
47
47
|
CONFIG_VERSION=""
|
|
48
48
|
CONFIG_REPO=""
|
|
49
|
-
if [ ! -
|
|
49
|
+
if [ ! -e "$CONFIG" ]; then
|
|
50
50
|
ERRORS+=("harness.config.yml not found at $REPO_ROOT. Run \`lemony install\`.")
|
|
51
|
+
elif [ ! -f "$CONFIG" ]; then
|
|
52
|
+
# There, but not a file awk can read: a directory, or a FIFO it would block on
|
|
53
|
+
# until something wrote to it. `-e`/`-f` follow a symlink, so a dead link stays
|
|
54
|
+
# "not found" above and a link to one of these lands here.
|
|
55
|
+
ERRORS+=("harness.config.yml at $REPO_ROOT is not a regular file (a directory, a FIFO, a socket or a device), so it was not read. Replace it with the config file.")
|
|
56
|
+
elif [ ! -r "$CONFIG" ]; then
|
|
57
|
+
# A file awk cannot open failed the key check below, so the boot blamed missing keys
|
|
58
|
+
# under a raw awk error.
|
|
59
|
+
ERRORS+=("harness.config.yml at $REPO_ROOT cannot be read (check its permissions), so it was not read.")
|
|
51
60
|
elif ! awk '
|
|
52
61
|
{ sub(/\r$/, "") }
|
|
53
62
|
/^[^[:space:]#]/ {
|
|
@@ -190,6 +199,19 @@ if [ "${#ERRORS[@]}" -eq 0 ] && [ -n "$GIT_USER_EMAIL" ]; then
|
|
|
190
199
|
CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
|
|
191
200
|
if [ -z "$USER_SLUG" ]; then
|
|
192
201
|
: # path-unsafe slug — pointer write skipped above
|
|
202
|
+
elif [ -L "$REPO_ROOT/.claude" ] || [ -L "$REPO_ROOT/.claude/state" ]; then
|
|
203
|
+
# A linked directory takes the pointer, and every refresh after it, out of the
|
|
204
|
+
# repo — the same write-through the dangling-link branch below refuses.
|
|
205
|
+
WARNINGS+=(".claude or .claude/state is a symlink, so the session start was not recorded (the pointer would be written wherever it points). Replace it with a directory.")
|
|
206
|
+
elif [ -e "$CURRENT_PATH" ] && [ ! -f "$CURRENT_PATH" ]; then
|
|
207
|
+
# There, but not a file: writing to a FIFO blocks until something reads it, and
|
|
208
|
+
# awk reading one blocks until something writes. The pointer is not critical, so
|
|
209
|
+
# the boot goes on without it — a warning, not a blocking error.
|
|
210
|
+
WARNINGS+=(".claude/state/current-$USER_SLUG.md is not a regular file (a directory, a FIFO, a socket or a device), so the session start was not recorded. Remove it; the next session recreates it.")
|
|
211
|
+
elif [ -L "$CURRENT_PATH" ] && [ ! -e "$CURRENT_PATH" ]; then
|
|
212
|
+
# A symlink whose target is gone: `-e`/`-f` follow it and see nothing, and the
|
|
213
|
+
# `cat >` below would create the target wherever the link points.
|
|
214
|
+
WARNINGS+=(".claude/state/current-$USER_SLUG.md is a symlink whose target is missing, so the session start was not recorded (writing through it would create the file it points at). Remove it; the next session recreates it.")
|
|
193
215
|
elif [ ! -f "$CURRENT_PATH" ]; then
|
|
194
216
|
mkdir -p "$REPO_ROOT/.claude/state"
|
|
195
217
|
cat > "$CURRENT_PATH" <<EOF
|
|
@@ -309,12 +309,13 @@ fi
|
|
|
309
309
|
# same recipe as playbook-scan.sh): emit `<key>\t<value>` for the uncommented
|
|
310
310
|
# `checks_timeout_secs` / `allow_no_checks` entries — inline comments stripped
|
|
311
311
|
# (on the block header too) and surrounding quotes removed. Any column-0 line
|
|
312
|
-
# ends the block. Absent/unreadable config → baked defaults.
|
|
312
|
+
# ends the block. Absent/unreadable/non-regular config → baked defaults.
|
|
313
313
|
CONFIG_TIMEOUT=""
|
|
314
314
|
CONFIG_ALLOW_NO_CHECKS=""
|
|
315
315
|
read_merge_config() {
|
|
316
316
|
local config="$ROOT/harness.config.yml"
|
|
317
|
-
|
|
317
|
+
# `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the merge.
|
|
318
|
+
{ [ -f "$config" ] && [ -r "$config" ]; } || return 0
|
|
318
319
|
|
|
319
320
|
local key value
|
|
320
321
|
while IFS=$'\t' read -r key value; do
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
# which would breach the per-fire budget) and with no materialized state file to
|
|
40
40
|
# go stale before an `update` command exists (P7). Falls back to the baked
|
|
41
41
|
# defaults when the config, its `paths` block, or a key is absent or commented
|
|
42
|
-
# out (fresh clone, the vendor repo dogfooding itself, an un-customized install)
|
|
42
|
+
# out (fresh clone, the vendor repo dogfooding itself, an un-customized install),
|
|
43
|
+
# and when the config is not a regular file.
|
|
43
44
|
_resolve_playbook_dirs() {
|
|
44
45
|
local repo_root="$1"
|
|
45
46
|
local home_dir="$2"
|
|
@@ -47,7 +48,8 @@ _resolve_playbook_dirs() {
|
|
|
47
48
|
PLAYBOOKS_GLOBAL_DIR="$home_dir/.claude/playbooks"
|
|
48
49
|
|
|
49
50
|
local config="$repo_root/harness.config.yml"
|
|
50
|
-
|
|
51
|
+
# `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the hook.
|
|
52
|
+
{ [ -f "$config" ] && [ -r "$config" ]; } || return 0
|
|
51
53
|
|
|
52
54
|
# One awk pass: within the top-level `paths:` block, emit `<key>\t<value>` for
|
|
53
55
|
# the (uncommented) `playbooks` / `playbooks_global` entries — inline comments
|
|
@@ -70,34 +70,67 @@ variables and the DTCG JSON:
|
|
|
70
70
|
- `name` — the variable's dotted path. Map it to/from the tool's own grouping separator.
|
|
71
71
|
- `value` for a literal; `ref` for an alias (the name of the variable it points at).
|
|
72
72
|
- `modes` — per-theme overrides (the base theme is `value`/`ref`, not repeated).
|
|
73
|
+
- Every `value` and mode value is a **string** — `"1024"`, not `1024` — and every `ref` a
|
|
74
|
+
non-empty one; each variable carries a `value` or a `ref` (a `value` beside a `ref` is
|
|
75
|
+
ignored), and `modes` is an object whose mode names are not blank nor `__proto__`. A name
|
|
76
|
+
is dot-separated segments, none empty, none starting with `$`, none `__proto__`, not just
|
|
77
|
+
a tier (`primitive`, `semantic`, `component`), and no two variables share one. The CLI
|
|
78
|
+
refuses a file that breaks any of these, naming the variable (and the mode, for a mode
|
|
79
|
+
value).
|
|
73
80
|
|
|
74
81
|
The CLI maps this to the 3-tier DTCG model: `ref` → an alias `{path}`, `modes` →
|
|
75
|
-
`$extensions["com.lemony.modes"]`, and the tier follows the path
|
|
76
|
-
|
|
82
|
+
`$extensions["com.lemony.modes"]`, and the tier follows the path. A name without a tier
|
|
83
|
+
lands on the one tier `docs/design-tokens.json` already has it in; otherwise a literal
|
|
84
|
+
defaults to `primitive` and an alias to `semantic`. A bare `ref` — or a bare `{alias}` mode
|
|
85
|
+
value — resolves to the tiered path its target lands at in the same neutral file, or to the
|
|
86
|
+
one tier the token file has it in.
|
|
77
87
|
|
|
78
88
|
## import — tool → JSON
|
|
79
89
|
|
|
80
90
|
1. **Read** the tool's variables over MCP and write them to a neutral file (a temp path).
|
|
81
91
|
2. **Preview** the diff (writes nothing):
|
|
82
92
|
`lemony design-tokens import --from=<neutral-file>`. It prints, per token, the target tier
|
|
83
|
-
and `new` / `changed` (
|
|
93
|
+
and `new` / `changed` (each thing that changes, old → new: the value, the `$type`, each
|
|
94
|
+
mode) / `unchanged`. When
|
|
95
|
+
`docs/design-tokens.json` already fails validation it adds a `Warning:` saying so: the
|
|
96
|
+
apply will refuse any slice that leaves it failing, so tell the human before curating.
|
|
84
97
|
3. **Present and curate.** Walk the human through it: which new tokens to take, which changes
|
|
85
98
|
to accept, and — where a tool-origin name is ambiguous — whether a literal belongs in
|
|
86
99
|
`primitive` or `semantic`. The human owns the slice.
|
|
87
100
|
4. **Apply** the agreed slice:
|
|
88
101
|
`lemony design-tokens import --from=<neutral-file> --apply --only=<dotted,paths>`.
|
|
89
102
|
This does the additive merge into `docs/design-tokens.json` deterministically (it
|
|
90
|
-
bootstraps the file if it does not exist yet
|
|
91
|
-
|
|
103
|
+
bootstraps the file if it does not exist yet and the slice writes something). A changed
|
|
104
|
+
token keeps what the neutral file does not carry — `$description`, a contrast pairing,
|
|
105
|
+
other extensions — and an unchanged one is left as it is. A token the tool cannot hold
|
|
106
|
+
(see export) is never written over: the preview shows it as a change from
|
|
107
|
+
`(outside the sync: <why>)`, and the apply lists it as not applied. It writes nothing
|
|
108
|
+
when the merged file would fail validation — typically a `semantic` alias whose
|
|
109
|
+
`primitive` target is neither in the file nor in the slice: add the target to the same
|
|
110
|
+
`--only`, or fix the file first. When the neutral file does not carry the target at
|
|
111
|
+
all, the refusal says so: create it in the tool first, or leave out what references
|
|
112
|
+
it. An `--only` path no variable lands at is refused rather than skipped. A refusal
|
|
113
|
+
prints the preview again, so the slice can be re-curated from the paths it lists. Then run
|
|
114
|
+
`lemony design-tokens validate` to confirm the result is well-formed.
|
|
92
115
|
|
|
93
116
|
## export — JSON → tool
|
|
94
117
|
|
|
95
|
-
1. **Plan.**
|
|
118
|
+
1. **Plan.** `export` refuses (exit 1) a `docs/design-tokens.json` that fails
|
|
119
|
+
`lemony design-tokens validate` — fix the file first. Read the tool's current variables
|
|
120
|
+
over MCP into a neutral file, then:
|
|
96
121
|
`lemony design-tokens export --tool-state=<tool-state> --out=<projection-file>`. It prints
|
|
97
122
|
the additive upsert plan (how many to create, how many to update — **never any deletes**)
|
|
98
123
|
and writes the projection the tool should hold to `<projection-file>`. The `--tool-state`
|
|
99
124
|
is optional; without it every variable is planned as a create (the upsert is idempotent by
|
|
100
|
-
name either way).
|
|
125
|
+
name either way). A tool variable named without its tier (`color.brand`) is matched to the
|
|
126
|
+
path `import` lands it at (`primitive.color.brand`), so when you push, write into that
|
|
127
|
+
existing variable rather than creating a tiered twin. A token a tool variable cannot hold
|
|
128
|
+
is not projected: a composite (a shadow, a typography, a cubic Bézier — an object or
|
|
129
|
+
array `$value`), a token whose modes the neutral format cannot carry (a mode value that
|
|
130
|
+
is not text, a number or a boolean, a blank or `__proto__` mode name, a modes extension
|
|
131
|
+
that is not an object), and any alias to one of those. The plan names each as "not
|
|
132
|
+
projected", with why — say so to the human. `--out` takes a file path (not a pipe, nor
|
|
133
|
+
the token file itself).
|
|
101
134
|
2. **Confirm.** Show the plan; the human approves.
|
|
102
135
|
3. **Push.** Write the projection's variables into the tool over MCP — create new ones, update
|
|
103
136
|
changed ones, leave tool-only variables untouched.
|
|
@@ -88,10 +88,13 @@ rollback:
|
|
|
88
88
|
# extensions (.css/.scss/.ts/.tsx/.vue/.svelte/.astro/.js/.mdx/.html/…). Add extra
|
|
89
89
|
# suffixes here for a stack the built-ins don't cover — additive, never a replacement.
|
|
90
90
|
# Default none.
|
|
91
|
-
# `verify`: a command line `design-tokens contrast` runs
|
|
91
|
+
# `verify`: a command line `design-tokens contrast` runs alongside its own WCAG checks
|
|
92
92
|
# (through `sh -c`, in the repo root) — your design system's own verifier for the rules
|
|
93
93
|
# only it can know (palette under colour-vision-deficiency simulation, property grammar,
|
|
94
|
-
# CSS scans). Its output is forwarded; a non-zero exit fails the gate.
|
|
94
|
+
# CSS scans). Its output is forwarded; a non-zero exit fails the gate. It runs on every
|
|
95
|
+
# path: with no docs/design-tokens.json only the WCAG pair check is skipped, and a file
|
|
96
|
+
# that cannot be read or parsed is reported alongside it — never the verifier you
|
|
97
|
+
# declared. Default none.
|
|
95
98
|
# design_tokens:
|
|
96
99
|
# scan_extensions:
|
|
97
100
|
# - .foo
|