grails-cli 0.3.0 → 0.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grails-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Grails Code Sync from the command line: reads a component library's code and uploads its design tokens and component specs to Grails.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://docs.grails.design/cli/overview/",
@@ -29,16 +29,32 @@ to choose when a new release reaches your pushes.
29
29
  | `grails-cli tokens [root] [--json]` | the design tokens a sync would upload (with `dtcg`: the files, or the exact upload with `--json`) | no |
30
30
  | `grails-cli tokens baseline [root] --out <dir> [--in-repo]` | writes the scanner's tokens as DTCG files and a resolver, a first version to own | no |
31
31
  | `grails-cli sync [root] [--dry-run] [--json]` | uploads the tokens, then the components | yes (with `--dry-run`: asks what would change, stores nothing) |
32
- | `grails-cli tokens pending [root] [--json]` | the token changes people asked for in Grails, before → after | reads only |
32
+ | `grails-cli tokens pending [root] [--collection <name>] [--request <id>] [--json]` | the token changes people asked for in Grails: type, before → after, and the id; the filters narrow it | reads only |
33
+ | `grails-cli tokens discard <change-id> \| --all [--collection <name>] \| --request <id> --reason "<why>" [--dry-run]` | closes one change, or a set in one step, that your team will not carry, with the reason | yes (Grails keeps the changes, closed); `--dry-run` changes nothing |
33
34
  | `grails-cli rules [root] [--set code=figma]` | the naming rules that match code tokens to Figma ones | edits need `write:tokens` |
34
- | `grails-cli status` | the project's code sources and their last syncs | reads only |
35
+ | `grails-cli connect [--prompt]` | keeps the API key in the operating system's keychain (checked against Grails first) so no command needs `GRAILS_API_KEY`; reads the clipboard and clears it, or with `--prompt` asks in a hidden prompt | reads only (one authenticated read to validate the key) |
36
+ | `grails-cli disconnect` | removes the key `connect` stored (the key itself is not revoked) | no |
37
+ | `grails-cli update [root]` | asks the npm registry now for the latest `grails-cli` and prints the command that updates it for the project's package manager; it does not run it | asks the npm registry only |
38
+ | `grails-cli status` | the project's code sources, their last syncs, and how many changes asked for in Grails wait for each | reads only |
35
39
  | `grails-cli chi [root]` | the Component Health Index of every component | no |
36
40
  | `grails-cli check-classes [root]` | Tailwind classes that are probably misspelt | no |
37
41
  | `grails-cli skill` | prints this file | no |
38
42
 
39
- **The API key** is read from `GRAILS_API_KEY` and from nowhere else. Never pass it as a flag, and never print the
40
- environment. To create one, go to Team Settings › Integrations › API Keys in Grails and give it the `write:code`
41
- scope. `GRAILS_API_URL` points the CLI at another API.
43
+ **The API key** is read from `GRAILS_API_KEY` in the environment (CI, and anyone who prefers it) and, when that is not
44
+ set, from the operating system's keychain, where `grails-cli connect` put it; and from nowhere else. Never pass it as a
45
+ flag or an argument, and never print the environment. To create one, go to Team Settings › Integrations › API Keys in
46
+ Grails and give it the `write:code` scope. `GRAILS_API_URL` points the CLI at another API; the keychain keeps one key
47
+ per API, by its host.
48
+
49
+ **An agent does not run `connect` for the person.** `connect` takes the key from the person's clipboard or from a hidden
50
+ prompt on their terminal; a key an agent was handed in a chat is already in a transcript, so it does not go to
51
+ `connect --prompt` either. When there is no key, tell the person to run `grails-cli connect` in their own terminal (or to set
52
+ `GRAILS_API_KEY`), and carry on with the commands that need none (`init`, `scan`, `tokens`, `sync --dry-run`).
53
+
54
+ **Updates.** Once a day the CLI asks the npm registry for the latest version and, while it is behind, every run ends with
55
+ one line on stderr naming the two versions and the update command. That line is not an error and does not change the
56
+ exit code. `GRAILS_CLI_NO_UPDATE_CHECK=1` turns it off. `grails-cli update` prints the command and never runs it: update
57
+ the version only when the person asks.
42
58
 
43
59
  ## `grails.code.json`
44
60
 
@@ -59,6 +75,9 @@ scope. `GRAILS_API_URL` points the CLI at another API.
59
75
  - `files` are globs of `.json` files.
60
76
  - `resolver` is optional, and is a DTCG resolver document. Its `$ref`s name the files relative to its own folder,
61
77
  so every file must sit in or below that folder.
78
+ - `cssVariables` is optional: how your CSS names the tokens, so Grails can tell which token a `var(--x)` is. It holds
79
+ the `prefix` of your build, the `exceptions` to the naming, and `ignore`, patterns for custom properties that are
80
+ not your tokens. See "Telling Grails which token a `var(--x)` is".
62
81
  - **`css`** (CSS files whose custom properties are tokens) and **`tokens`** (TypeScript token objects) are for the
63
82
  scanner, when there is no `dtcg`.
64
83
 
@@ -74,8 +93,119 @@ scope. `GRAILS_API_URL` points the CLI at another API.
74
93
  being the source. The command lists the values the scanner left out (they are not in the files), refuses to run while
75
94
  `dtcg` is set, and never writes into a folder that has files.
76
95
 
77
- With DTCG, a sync records which components each component renders, but **not which tokens it uses**: nothing tells
78
- Grails which DTCG token a style's `var(--x)` is. The sync says so in its output.
96
+ With DTCG, a sync records which components each component renders, and which tokens it uses **only through a `var(--x)`
97
+ that `cssVariables` ties to a token** (next section). Every `var(--x)` it cannot tie is listed, so you can fix it.
98
+
99
+ ## Telling Grails which token a `var(--x)` is
100
+
101
+ *This section is written for a coding agent that is asked to set it up. Follow the steps in order, and never guess a
102
+ name.*
103
+
104
+ Your components write `var(--color-brand-500)`; your DTCG files say `color.brand.500`. With `dtcg` declared, the scanner
105
+ reads no token, so the only thing that ties the two is how your build names a token's custom property. Grails assumes
106
+ **Style Dictionary's default**, the `css` transform group (`name/kebab`):
107
+
108
+ - the token's path segments, joined with `-`, lower case, behind two dashes: `color.brand.500` is `--color-brand-500`;
109
+ - with the build's `prefix` first: `prefix: "ds"` makes it `--ds-color-brand-500`;
110
+ - camelCase is split (`fontSize` → `font-size`, `h1Bold` → `h1-bold`), but **a digit stays with its letters** (`2xl`
111
+ stays `2xl`, `body2` stays `body2`);
112
+ - anything that is not a letter or a digit is a separator (`Brand Primary`, `brand_primary` and `brand-primary` all give
113
+ `brand-primary`);
114
+ - the name is **case-sensitive**: `var(--Color-Brand-500)` is no token, because the build writes lower case.
115
+
116
+ Where a name does not follow that, you write an **exception**: the custom property and the token it is. A custom
117
+ property that is **not your token at all** (a library's, the framework's) goes in **`ignore`**, as a pattern.
118
+
119
+ ```json
120
+ {
121
+ "dtcg": {
122
+ "files": ["tokens/**/*.json"],
123
+ "resolver": "tokens/resolver.json",
124
+ "cssVariables": {
125
+ "prefix": "ds",
126
+ "exceptions": { "--brand": "color.brand.500", "--surface": "color.semantic.surface" },
127
+ "ignore": ["--radix-*", "--tw-*"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ All three keys are optional. An exception's value is the token's dotted path in the files (the first segment is its
134
+ collection). **An exception wins over the convention**, and one that names a path the files do not have is reported.
135
+
136
+ **`ignore` is a list of patterns**: a name as `var()` writes it, two dashes first, with `*` for the part that varies
137
+ (`*` is the only wildcard, and at most 200 patterns). A name an `ignore` pattern covers is not resolved, not counted,
138
+ not listed and never sent, **even when the convention would have found a token of that name**; an exception for the same
139
+ name wins over `ignore`. A pattern that covers nothing the components use is not an error: `--json` lists it in
140
+ `css_variables.ignore_unused`, and the terminal line says how many were ignored (`· 14 ignored`), never which.
141
+
142
+ **The steps:**
143
+
144
+ 1. **Look at how the repository builds its CSS from the DTCG files.** Find the Style Dictionary configuration
145
+ (`config.json`, `style-dictionary.config.*`, `sd.config.*`, or a script that calls it) and read, for the CSS platform,
146
+ its `prefix`, its `transformGroup` and its `transforms`. Then open the CSS it generates and look at a few real names:
147
+ a name in the output beats a reading of the configuration.
148
+ - `transformGroup: "css"` (or `name/kebab` in `transforms`), and no custom `name` transform: the convention applies.
149
+ - `name/camel`, `name/snake`, `name/constant`, `name/pascal` or a custom `name` transform: the names do not follow
150
+ the convention, and each one a component uses needs an exception.
151
+ 2. **Set the prefix**: `"cssVariables": { "prefix": "<the build's prefix>" }`, and leave it out when the build has none.
152
+ 3. **Run `grails-cli sync --dry-run`.** It needs no key and sends nothing (with a key it also asks Grails what
153
+ would change). `--json` adds a `css_variables` object with the same list. The line `css variables` counts the custom
154
+ properties your components use and how many resolve to a token.
155
+ 4. **Read the unresolved list** (the `!` lines under it), and decide each one:
156
+ - `var(--x) is no token of the DTCG files`: find where `--x` comes from, and choose by who owns it.
157
+ - **It is your token under another name**: generated from a DTCG token, or an alias of a generated one
158
+ (`--x: var(--ds-color-brand-500)`). Add it to **`exceptions`** with that token's path. An exception says what a
159
+ name IS.
160
+ - **It is not your token, and never will be**: a library's, the framework's, one the page sets at run time. Add a
161
+ pattern that covers the family to **`ignore`**, so the list holds only what a person still has to decide.
162
+ Examples that are common: Radix sets sizes for its popovers and menus
163
+ (`--radix-dropdown-menu-content-available-height`, `--radix-select-trigger-width`: pattern `--radix-*`);
164
+ Tailwind's own internals, which a component reads when it writes a ring or a shadow by hand
165
+ (`--tw-ring-color`, `--tw-ring-offset-color`: pattern `--tw-*`); a value the page sets from JavaScript (`--vh`,
166
+ `--scroll-y`). A custom property one component declares for itself (`--card-padding`) is also not a token: ignore
167
+ it, by name or by a prefix that component's team already uses.
168
+ - **You cannot tell which of the two it is**: do not ignore it and do not guess an exception. Leave it listed and
169
+ ask the person.
170
+ - `var(--x) is the name of N tokens`: two tokens spell the same CSS name (`brandPrimary` and `brand-primary`).
171
+ Read the generated CSS to see which one the build keeps (the last declaration wins), and add that one to
172
+ `exceptions`. Better, rename one of them in the DTCG files.
173
+ - `dtcg.cssVariables.exceptions: --x names color.y, which is not a token of the DTCG files`: the exception is
174
+ mistyped, or the token was renamed or removed. Fix its path (`grails-cli tokens --json` prints the files) or
175
+ delete the exception.
176
+ - `whose name has a "/"`: a level of that token's name contains a `/`, which a usage upload cannot spell. Rename the
177
+ level in the DTCG files.
178
+ 5. **Run step 3 again** until every remaining `!` line is one you decided to leave for the person. Then tell them what
179
+ you added to `exceptions` and to `ignore` and what you left, with the reason for each.
180
+ 6. **Prune** with `grails-cli sync --dry-run --json`: `css_variables.ignore_unused` lists the patterns that ignored
181
+ nothing (no component uses a name they cover, or an exception already names every such name). Delete those.
182
+
183
+ **Keep `ignore` narrow.** A pattern beats the naming convention, so `--*-bg` would hide a real token that happens to end
184
+ in `-bg`: the token would no longer be tied to the components that use it, and the line would not say so. Cover a family
185
+ by its owner's prefix (`--radix-*`), not by a shape (`--*-width`). If a name the pattern covers IS a token, add an
186
+ exception for it: that is the way to keep one inside a broad pattern.
187
+
188
+ **What this reads, and what it does not.** The scanner reads a `var(--x)` a component can use in three places:
189
+
190
+ - **the `style` of any element** in the component, every property (`margin`, `border`, `boxShadow`, …) and not only
191
+ colour, padding and size; also the props libraries use for styles (`contentContainerStyle`, `css`, `sx`), a constant or
192
+ style sheet the style refers to, and every branch of a condition or lookup;
193
+ - **the stylesheet the component's file imports** (`.css`, CSS modules, `.scss`, `.less`), for every component that file
194
+ holds. A custom property a rule *declares* in `:root`, a theme selector or `@theme` is a definition, not a use, so a
195
+ tokens sheet imported by a layout does not make the layout "use" every token. `@import` inside a stylesheet is not
196
+ followed;
197
+ - **Tailwind classes** in `className` and in `cn`, `clsx`, `classnames`, `cx`, `twMerge` and `cva` calls, variants
198
+ included: `bg-[var(--x)]` in either version, `bg-(--x)` in Tailwind 4, `bg-[--x]` in Tailwind 3. (Tailwind 4
199
+ compiles `bg-[--x]` to a literal `--x`, not a `var()`, so it is not read there.)
200
+
201
+ It does **not** read a name a template assembles at run time (`` `var(--space-${n})` ``), a class helper it does not know
202
+ (`tv()`), CSS-in-JS template literals (styled-components, Emotion's `css` tag), a component a file defines but does not
203
+ export, or a property set from JavaScript. So a custom property missing from the list may still be used. A reference
204
+ that is not a `var()` (a Tailwind theme class such as `bg-primary`, a TypeScript token object) is counted in a line ("N
205
+ token reference(s)… are not linked") but not tied to a token. A run never fails for any of this.
206
+
207
+ **Do not** edit the token files or the CSS to make a line disappear: an exception, or a pattern in `ignore`, is the
208
+ team's statement of what a name is, or that it is not theirs.
79
209
 
80
210
  ## Writing DTCG that Grails accepts
81
211
 
@@ -110,21 +240,78 @@ Grails reads the **DTCG format 2025.10** (designtokens.org) and validates every
110
240
  - **Booleans, copy strings, blurs and layout grids.** Use no `$type`, a readable string `$value` (`"true"`,
111
241
  `"blur(4px)"`, `"12 columns, 24px gutter"`), and the real value in
112
242
  `design.grails.value: { "type": "boolean" | "string" | "blur" | "grid", "value": … }`.
243
+ - **A colour built from a colour and an opacity token.** Keep the applied colour in `$value`, and name the parts in
244
+ `design.grails.composed: { "color": "{path}" or a DTCG colour, "opacity": "{path}" }`. The opacity token is read
245
+ as it is: a Figma opacity is 0–100, so divide by 100.
113
246
 
114
- Grails keeps these extensions with each token. Interpreting them is still being built, so write them now, and nothing
115
- is lost when it lands.
247
+ **What Grails does with them.**
248
+ - It reads a token with `design.grails.value` as that type and that value. The extension wins over `$value`, so a
249
+ `$value` like `"{brand}"` there is text, not a reference.
250
+ - It validates the forms above. A malformed one (a boolean whose value is `"yes"`, a `textCase` of `"shout"`) refuses
251
+ the sync with `extension_invalid` and the pointer. A `{path}` inside them that names no token is `alias_unknown`.
252
+ - What it does not know (a key a form does not have, a value type it has never seen) is kept as written, with the
253
+ warning `extension_not_interpreted`, so a document a newer tool wrote loses nothing.
116
254
 
117
255
  When a sync is refused, each finding names the file, the JSON pointer inside it and a code (`schema_invalid`,
118
- `token_at_root`, `legacy_format`, `alias_unknown`, `alias_cycle`, …). Fix the source and sync again.
256
+ `token_at_root`, `legacy_format`, `alias_unknown`, `alias_cycle`, `extension_invalid`, …). Fix the source and sync
257
+ again.
119
258
 
120
259
  ## Changes asked for in Grails
121
260
 
122
261
  Admins and editors can ask for token changes in Grails: a new value, a new token, collection or mode, or a removal.
123
- `grails-cli tokens pending` lists them by collection, as `before → after` in DTCG. Carry each one into your token
124
- source, or into the rules that generate it.
262
+ **Grails never writes your repository, and neither does the CLI.** Closing the loop takes three steps, and the last one
263
+ is automatic:
264
+
265
+ 1. `grails-cli tokens pending` lists the changes by collection: each with its **type**, `before → after` as DTCG, and
266
+ its **id**. The `before` is the value your repository holds now, resolved (an alias in your code shows as the colour
267
+ it resolves to). `--json` has every value exact.
268
+ 2. Carry each change into your token source, or into the rules that generate it, and merge it as any other change.
269
+ 3. The next `grails-cli sync` (in CI, on the declared branch) **marks the change applied** once your repository holds
270
+ its value, and prints how many. Grails compares the resolved value of the token in that mode, with the tolerance the
271
+ Diff uses for colours and sizes, so an alias that resolves to the asked value counts. A token you removed closes
272
+ its change; a created token, collection or mode closes when it exists.
273
+
274
+ To see what the next sync would do **before** it runs, use `grails-cli sync --dry-run` (with the key): it prints how many
275
+ changes it would mark applied, close because their token is gone, or leave open, with their ids, and stores nothing.
276
+ `grails-cli status` shows how many changes wait for each source.
277
+
278
+ **If the repository holds another value, the change is not closed.** Grails keeps proposing its value, checks again on
279
+ every sync, and records what your code holds: running `tokens pending` again after a sync shows the real "before".
280
+
281
+ **A change your team will not carry is closed with a reason, not ignored:**
282
+
283
+ ```sh
284
+ grails-cli tokens discard <change-id> --reason "Handled in the design-tokens repo"
285
+ ```
286
+
287
+ - The id is the one `tokens pending` prints beside the change. Never invent one.
288
+ - `--reason` is required: 1 to 1000 characters on one line. Say why, in words a designer reads: Grails keeps it with the
289
+ change, with who discarded it (the owner of the API key) and that it came from code.
290
+ - Discarding a collection takes its modes and tokens with it. One value of a token being created cannot be discarded
291
+ alone: discard the token.
292
+ - Exit 3 means Grails has no open change with that id for this source, or it was already applied or discarded. Do not
293
+ retry; list again with `tokens pending`.
294
+ - Do it only when the team decided not to carry the change. If it only has not been carried yet, leave it: the sync
295
+ closes it when it arrives.
296
+
297
+ **Many at once.** Exactly one of three picks the changes (the reason is required, as above):
298
+
299
+ ```sh
300
+ grails-cli tokens discard --all --dry-run # look first: nothing is changed
301
+ grails-cli tokens discard --all --reason "Handled in the tokens repo" # everything waiting for this repository
302
+ grails-cli tokens discard --all --collection color --reason "…" # one collection's
303
+ grails-cli tokens discard --request <request-id> --reason "…" # one change request's
304
+ ```
125
305
 
126
- ⚠️ For now, a change you carried over still shows as pending in Grails. Marking it applied when a sync shows the
127
- repository says the same is designed but not built yet.
306
+ - `--dry-run` lists the changes that would be discarded (id, token, before → after) and changes nothing. Run it first
307
+ and read the list: there is no `--yes`, and a discard cannot be undone from the CLI.
308
+ - The set is closed all together or not at all, and at most 1000 at once. Past that the CLI says so and discards
309
+ nothing: narrow it with `--collection` or `--request`.
310
+ - `tokens pending --collection <name>` and `--request <id>` list exactly what the same words would discard. The request
311
+ id is printed under the heading of `tokens pending`.
312
+ - `--collection` goes with `--all` only. Naming a change id and `--all` together is refused.
313
+ - The answer says how many were discarded; `--json` has the ids. Exit 3 means a request or source Grails does not hold
314
+ (nothing was discarded) or a set over the cap.
128
315
 
129
316
  ## In CI
130
317