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/README.md CHANGED
@@ -14,8 +14,8 @@ npx grails-cli scan /path/to/library # the manifest of components
14
14
  npx grails-cli tokens /path/to/library # the design tokens Grails would read; sends nothing
15
15
  npx grails-cli sync /path/to/library --dry-run # what would be uploaded; sends nothing, needs no key
16
16
 
17
- export GRAILS_API_KEY=grails_sk_… # see "The key"
18
- npx grails-cli sync /path/to/library
17
+ npx grails-cli connect # once: keeps the API key you copied in the system keychain; see "The key"
18
+ npx grails-cli sync /path/to/library # in CI, GRAILS_API_KEY from a secret does the same job
19
19
  npx grails-cli status
20
20
  ```
21
21
 
@@ -32,9 +32,13 @@ The examples below write `grails-cli`; use `npx grails-cli` if it is not install
32
32
  | `tokens [root] [--profile f] [--json]` | The design tokens Grails would read (a Tailwind theme, CSS listed in the profile's `css`, token sources in its `tokens`), and every value left out. `--json` is the exact token list a sync uploads. Sends nothing, needs no key. |
33
33
  | `tokens baseline [root] --out <dir> [--in-repo] [--profile f]` | The scanner's tokens as DTCG files and a resolver, a first DTCG version for the team to own (see "Moving from the scanner to DTCG"). Writes only where `--out` says, refuses a path inside a git repository unless `--in-repo`, and never touches a folder that has files. Sends nothing, needs no key. |
34
34
  | `sync [root] [--dry-run] [--json] [--trigger ci\|local] [--profile f] [--tier …] [--name n] [--platform p]` | **Tokens first, then components, in one step.** Records the tokens in one request, then opens a component sync, sends the components, finalizes, and prints the result and the vault link. See "Exit codes". |
35
- | `status` | The code sources of the key's project, with the last component sync and the last token sync of each. |
36
- | `tokens pending [root] [--name n] [--limit 1..1000] [--json]` | The token changes people asked for in Grails, by collection, before → after. Writes nothing (see "Changes asked for in Grails"). |
35
+ | `status` | The code sources of the key's project, with the last component sync and the last token sync of each, and how many changes asked for in Grails wait for it. |
36
+ | `tokens pending [root] [--name n] [--collection c] [--request id] [--limit 1..1000] [--json]` | The token changes people asked for in Grails, by collection, with each one's type, before → after and id; `--collection` and `--request` narrow it. Writes nothing (see "Changes asked for in Grails") |
37
+ | `tokens discard <change-id> \| --all [--collection c] \| --request id` `--reason "…" [--dry-run] [root] [--name n] [--json]` | Closes one change, or many in one step, that your team will not carry into the repository, with the reason; `--dry-run` lists what would go and changes nothing (see "Changes asked for in Grails") |
37
38
  | `rules [root] [--set code=figma \| --delete code \| --threshold n] [--json]` | The naming rules of the Code ↔ Figma match (see "Naming rules"). |
39
+ | `connect [--prompt]` | Keeps your API key in the operating system's keychain, so no command needs `GRAILS_API_KEY` in your shell. Reads the key from the clipboard (then clears it) or, with `--prompt`, from a prompt that shows nothing as you type; checks it against Grails first, and stores nothing if Grails refuses it. See "The key". |
40
+ | `disconnect` | Removes the key `connect` stored for the API `GRAILS_API_URL` points at. |
41
+ | `update [root]` | Asks the npm registry now for the latest `grails-cli` and prints the installed version, the latest, and the command that updates it for your package manager. It does not run it. Exits 4 when the registry cannot be reached. See "Updates". |
38
42
  | `skill` | Prints the agent skill the package ships (`templates/skill/grails-cli/SKILL.md`), for a team to save where its coding agent reads skills. Sends nothing, writes nothing. |
39
43
 
40
44
  ### `sync`
@@ -55,7 +59,9 @@ storing routes refuse a CLI older than 0.2.0 with 400 `GRAILS_CODE_CLI_OUTDATED`
55
59
  recorded with `POST /v1/code/token-syncs` in one request. Grails builds the DTCG document from it and answers what
56
60
  changed against the source's previous snapshot; the same content is stored once. A token step that fails (a
57
61
  project Grails cannot read, the server refusing the list) does **not** stop the components: they sync anyway, and
58
- the run exits with the token step's code. Then the components:
62
+ the run exits with the token step's code. Once the tokens are recorded, Grails also compares the changes people asked
63
+ for in Grails with what the repository now holds, marks the ones it satisfies applied, and the sync says how many (see
64
+ "Changes asked for in Grails"); that comparison never fails the sync. Then the components:
59
65
 
60
66
  1. **Opens** the sync with every component key the scan found (`declared_keys`). That is how the server tells a
61
67
  component deleted from the code from one that failed to arrive.
@@ -83,10 +89,12 @@ when `CI=true` and `local` otherwise.
83
89
 
84
90
  ### `--dry-run`, `--json` and the job summary
85
91
 
86
- - **`--dry-run` with `GRAILS_API_KEY` set** asks Grails what each step would change, through the preview routes
92
+ - **`--dry-run` with a key** (`GRAILS_API_KEY`, or the one `connect` kept) asks Grails what each step would change, through the preview routes
87
93
  (`POST /v1/code/token-syncs:preview`, `POST /v1/code/syncs:preview`), which store nothing: tokens added, changed
88
94
  and removed; components added, changed, reactivated and **those that would be deleted**, with the same floor the
89
- real sync applies.
95
+ real sync applies. It also says what the real sync would do with the changes asked for in Grails (see below): how many
96
+ it would mark applied, close because their token is gone, or leave open, with their ids (`tokens.preview.token_changes`
97
+ in `--json`, and in the job summary). The same verdict the real sync applies, written nowhere.
90
98
  - **`--dry-run` without the key** counts locally and sends nothing, as it always did.
91
99
  - **`--json`** prints one report on stdout (`mode`, `tokens`, `components`, `exit_code`); the human lines go to
92
100
  stderr, so the output can be piped.
@@ -128,16 +136,50 @@ and the ones Grails suggests from tokens with the same value and one different w
128
136
  In Grails, an admin or an editor can ask for changes to this repository's tokens: a new value, a new token,
129
137
  collection or mode, a removal. **Grails never writes your repository, and neither does this CLI.**
130
138
 
131
- grails-cli tokens pending # by collection, before → after, as DTCG values
139
+ grails-cli tokens pending # by collection: type, before → after as DTCG values, and the id
132
140
  grails-cli tokens pending --json # the list as Grails answers it, every value exact
133
141
  grails-cli tokens pending --limit 1000 # the read answers 500 by default, at most 1000
142
+ grails-cli tokens pending --collection color # only that collection's changes
143
+ grails-cli tokens pending --request <request-id> # only one change request's (its id is under the heading)
134
144
 
135
145
  Carry each change into your token source, or into the rules that generate it (a Style Dictionary build, a Tokens
136
- Studio export, your CSS). ⚠️ **A change carried over still shows as pending for now:** marking it applied when a sync
137
- shows the repository says the same is designed but not live yet (it comes with the token comparison in the sync).
146
+ Studio export, your CSS). The loop closes by itself:
147
+
148
+ - **The next `sync` marks a change applied** once your repository holds its value. Grails compares the **resolved**
149
+ value of the token in that mode (an alias counts by the value it resolves to), with the tolerance the Diff uses for
150
+ colours and sizes. A token you removed closes its change; a created token, collection or mode closes when it exists.
151
+ The sync prints how many (`token_changes` in `--json`).
152
+ - **If the repository holds another value, the change stays.** Grails keeps proposing its value, checks again on every
153
+ sync, and records what your code holds. The "before" that `tokens pending` prints is what your repository holds now.
154
+ - **A change your team will not carry is closed with a reason:**
155
+
156
+ grails-cli tokens discard <change-id> --reason "Handled in the design-tokens repo"
157
+
158
+ The id is the one `tokens pending` prints beside the change. Grails keeps the change, closed, with who discarded
159
+ it (the owner of the API key: it is the only identity the API can verify), that it came from code, and the reason,
160
+ which is required (1 to 1000 characters, one line, no control characters). Discarding a collection takes its modes
161
+ and tokens with it; one value of a token being created cannot go alone, discard the token. A change that is
162
+ already applied or discarded answers exit 3.
163
+
164
+ - **Many at once, in one step.** Exactly one of three picks the changes, and the reason is required:
165
+
166
+ grails-cli tokens discard --all --reason "Handled in the design-tokens repo" # everything waiting for this repository
167
+ grails-cli tokens discard --all --collection color --reason "…" # one collection's
168
+ grails-cli tokens discard --request <request-id> --reason "…" # one change request's
169
+
170
+ The set is closed all together or not at all, and at most 1000 at once (more is refused with the way out: a
171
+ collection, or a request). **`--dry-run` lists what would go (id, token, before → after) and changes nothing**; the
172
+ reason is optional then. There is no `--yes`: the reason is the commitment. The answer, and `--json`, say how many
173
+ were discarded and which. `--collection` goes with `--all` only, and the same words select the same changes in
174
+ `tokens pending`, so you can look before you close. A change someone discards in Grails is a different path and is
175
+ deleted; these stay, with the reason.
176
+ - **A dry-run sync shows what the next sync would close:** `grails-cli sync --dry-run` (with the key) prints how many
177
+ changes it would mark applied, close because their token is gone, or leave open, and their ids, without storing
178
+ anything. `grails-cli status` shows how many changes wait for each source.
138
179
 
139
180
  An sRGB colour shows as its hex and a dimension as `24px`; anything else shows as its DTCG JSON, and `--json` has the
140
- exact components. The source is named as `sync` names it (`--name`, else the package name). It needs the usual key (`write:code`).
181
+ exact components. The source is named as `sync` names it (`--name`, else the package name); a change must belong to it.
182
+ Both commands need the usual key (`write:code`).
141
183
 
142
184
  ### Exit codes
143
185
 
@@ -149,9 +191,9 @@ red.
149
191
  |---|---|---|
150
192
  | 0 | done: synced, nothing changed, or a dry run | – |
151
193
  | 1 | the component sync ran and ended incomplete (unreadable components, or deletions held back) | no: read the warnings |
152
- | 2 | misconfigured: a flag, `grails.code.json` (including no declared `branch` on a real sync), no key, or a project Grails cannot read (its own tailwindcss missing, or of a major other than 3 and 4) | no: fix it |
153
- | 3 | the server refused it: the key, the plan, content it does not accept, or a CLI older than the minimum (400) | no: fix it |
154
- | 4 | the server could not be reached or failed (network, 5xx, a 429 after one retry) | yes |
194
+ | 2 | misconfigured: a flag, `grails.code.json` (including no declared `branch` on a real sync), no key, or a project Grails cannot read (its own tailwindcss missing, or of a major other than 3 and 4); for `connect`, a key of the wrong shape, an empty or unreadable clipboard, or no keychain on the machine | no: fix it |
195
+ | 3 | the server refused it: the key (also the one `connect` was given), the plan, content it does not accept, or a CLI older than the minimum (400) | no: fix it |
196
+ | 4 | the server could not be reached or failed (network, 5xx, a 429 after one retry); for `update`, the npm registry | yes |
155
197
 
156
198
  ### Where the tokens come from
157
199
 
@@ -170,8 +212,52 @@ red.
170
212
  - JSON that does not parse.
171
213
  - **What the API refuses comes back as findings** (422, or 413 for a limit). The run prints each one with the file the
172
214
  person knows and its JSON pointer, and exits 3.
173
- - **Usage.** Component uses still go with the finalize. Token uses do not: nothing tells which DTCG token a style's
174
- `var(--x)` is, and guessing it from the spelling would be a heuristic, so the CLI does not. The run says so.
215
+ - **Usage.** Component uses go with the finalize, and so do the token uses a `var(--x)` can be tied to. Nothing in the
216
+ files says which token a custom property is, so the team says how its CSS is named:
217
+
218
+ "dtcg": { "files": [...], "cssVariables": { "prefix": "ds", "exceptions": { "--brand": "color.brand.500" }, "ignore": ["--radix-*", "--tw-*"] } }
219
+
220
+ - **The convention** is Style Dictionary's default, its `css` transform group (`name/kebab`): the token's path
221
+ segments joined with `-`, lower case, behind the build's `prefix` (`color.brand.500` is `--ds-color-brand-500`).
222
+ A digit stays with its letters (`2xl`, `body2`), camelCase is split, any other character is a separator, and a name
223
+ is case-sensitive. `cssVariableName` in `@grails/dtcg` is that rule, pinned against the real Style Dictionary.
224
+ - **The direction is token → name.** A name does not say its token (`brandPrimary` and `brand-primary` are two tokens and
225
+ one name), so the CLI names every token of the upload and looks a variable up in that index. A name
226
+ two tokens share is reported as ambiguous, never given to one of them.
227
+ - **`exceptions`** map a custom property that does not follow the convention to the token it is. They win over the
228
+ convention, and an exception whose token is not in the upload is reported (and the name does not fall back to the
229
+ convention).
230
+ - **`ignore`** is a list of patterns for custom properties that are not the team's tokens at all (a library's, the
231
+ framework's): `["--radix-*", "--tw-*"]`. A pattern starts with `--`, `*` is the only wildcard, and there are at most
232
+ 200. A name it covers is taken out first: not resolved (so not sent, **even when the convention would have found a
233
+ token of that name**), not counted as used, not listed. An exception for the same name wins over `ignore`: it names a
234
+ token on purpose. A pattern that covers nothing used is not an error; `--json` lists it in
235
+ `css_variables.ignore_unused` so the team can delete it. The terminal line says how many were ignored, never which.
236
+ Use `exceptions` for what a name IS, `ignore` for "this is not ours".
237
+ - **The files are read here** with `readDtcg`, the reader the API runs, to know the tokens: `sync --dry-run` lists the
238
+ result with no key and sends nothing. A token goes to the API as `{ collection, name }` (the first level of its
239
+ path, and the rest joined with `/`), which the server turns back into the path with `tokenPath`; a token with a `/`
240
+ inside a level cannot be spelt that way and is reported.
241
+ - **Every `var(--x)` that resolves to no token is listed** (the terminal prints 20, `--json` carries 200 in
242
+ `css_variables`, the job summary folds them), with an exception that names a missing token. It never changes the
243
+ exit code.
244
+ - **What it reads.** A `var(--x)` a component can use, in three places, as `uses` outside the spec
245
+ (no hash moves, and the manifest keeps its shape and its version):
246
+ - the `style` of any element of the component, **every property** (`margin`, `border`, `boxShadow`, …), also `*Style`,
247
+ `css` and `sx` props, a constant or style sheet it refers to, and every branch of a condition or lookup;
248
+ - **the stylesheet its file imports** (`.css`, CSS modules, `.scss`, `.less`, `.pcss`), for every component that file
249
+ holds; a file two files import counts for both. Each file is read once per scan, only inside the project and never
250
+ under `node_modules`, at most 1 MiB, and 16 MiB of CSS in all (what is left out is a warning). `@import` is **not**
251
+ followed: a stylesheet that imports a tokens sheet would hand the component the aliases of the whole token layer.
252
+ A custom property a rule *declares* in `:root`, a theme selector or `@theme` is a token's definition, not a use;
253
+ - **Tailwind classes** in `className` and in `cn`, `clsx`, `classnames`, `cx`, `twMerge` and `cva` calls, variants
254
+ included: `bg-[var(--x)]` in either major, `bg-(--x)` (also `bg-(color:--x)`) in Tailwind 4 and `bg-[--x]` in
255
+ Tailwind 3. Measured on 4.2.1 and 3.4.17: 4 compiles `bg-[--x]` to a literal `--x`, and 3 does not know `bg-(--x)`,
256
+ so each major reads the form it compiles. With no Tailwind version found, the 4 form is read.
257
+ - **What it does not read.** A name a template assembles at run time, a class helper it does not know (`tv()`),
258
+ CSS-in-JS template literals, a component its file defines but does not export, a property set from JavaScript. Other
259
+ kinds of token reference (a Tailwind theme class such as `bg-primary`, a TypeScript token object) are counted, not
260
+ tied. A `bg-[var(--x)]` stays a literal colour in the spec itself, as before: only `uses` changed.
175
261
 
176
262
  **Without `dtcg`, the scanner reads** (a convenience for a first sync):
177
263
 
@@ -215,15 +301,57 @@ add and lists the values left out, which are not in the files: the ones the scan
215
301
 
216
302
  ## The key
217
303
 
218
- `GRAILS_API_KEY` is read from the environment **and from nowhere else**. A flag would land in the shell history,
219
- in the process list and in a CI log that prints the command line, so the CLI refuses any flag that looks like a
220
- credential (`--api-key`, `--token`, …) before it reads anything. It goes in one header, is never printed, and is
221
- scrubbed from everything the CLI prints in case an upstream message echoes it.
304
+ The CLI reads the API key from two places, in this order, and from nowhere else:
305
+
306
+ 1. **`GRAILS_API_KEY` in the environment**: CI, and anyone who prefers it. When it is set the keychain is not even asked.
307
+ 2. **The operating system's keychain**, where `grails-cli connect` put it.
308
+
309
+ With neither, a command stops with exit 2 and says to run `grails-cli connect` or set `GRAILS_API_KEY`.
310
+
311
+ A flag would land in the shell history, in the process list and in a CI log that prints the command line, so the CLI
312
+ refuses any flag that looks like a credential (`--api-key`, `--token`, …), and any argument that is itself a key,
313
+ before it reads anything. The key goes in one header, is never printed, and is scrubbed from everything the CLI
314
+ prints in case an upstream message echoes it.
222
315
 
223
316
  Create it in **Team Settings › Integrations › API Keys** with the `write:code` scope. It is a personal key: if its
224
317
  owner is demoted to viewer or leaves the team the key is **deleted**, and the API answers it like a key that never
225
318
  existed (`GRAILS_AUTH_INVALID_TOKEN`). The CLI says so in those words.
226
319
 
320
+ ### `connect` and `disconnect`
321
+
322
+ The API shows a key once, when it is created. Copy it, then:
323
+
324
+ ```bash
325
+ grails-cli connect # reads the clipboard
326
+ grails-cli connect --prompt # asks for it instead; nothing is shown as you type, and the clipboard is not used
327
+ grails-cli disconnect # removes it from the keychain
328
+ ```
329
+
330
+ - **It checks the key before keeping it.** The shape first (`grails_sk_` and 43 characters), then the same
331
+ authenticated read `status` makes. It prints the code sources of the key's project; Grails does not say the team's
332
+ name yet. A key Grails refuses (unknown, revoked, expired, without the `write:code` scope) stores **nothing**, and so
333
+ does one it could not ask about because the API was unreachable.
334
+ - **The clipboard is cleared once it has been read, whatever came of the key**, because a key Grails refuses may still be
335
+ a real one (for another environment, say). It is cleared only if it still holds what was read, so something you
336
+ copied meanwhile is left alone, and a clipboard with nothing like a key in it is not touched. Clipboard managers can
337
+ keep a history of what was copied, and nothing here can undo that: `--prompt` avoids the clipboard altogether.
338
+ - **No keychain, no key stored.** On a machine with none (a headless Linux box without `secret-tool`, a platform
339
+ without an integration) `connect` says so and stores nothing: use `GRAILS_API_KEY`. It checks this before it reads the
340
+ clipboard, so the key you copied is still there, and it never writes the key to a file.
341
+ - **The key never goes in an argument**, which every process on the machine can read. It reaches the keychain on standard
342
+ input of the platform's own tool, so there is no native module in the bundle:
343
+
344
+ | Platform | Keychain | Clipboard |
345
+ |---|---|---|
346
+ | macOS | the Keychain, through `/usr/bin/security -i` (the command is written to its stdin) | `pbpaste`, `pbcopy` |
347
+ | Linux | the Secret Service (GNOME Keyring, KWallet), through `secret-tool` from `libsecret-tools` | `wl-paste` and `wl-copy`, else `xclip` |
348
+ | Windows | the Credential Manager's `PasswordVault`, through Windows PowerShell (the key on its stdin) | `Get-Clipboard`, `Set-Clipboard` |
349
+
350
+ - **One entry per API.** The service is `grails-cli` and the account is the host of `GRAILS_API_URL`, so a staging key
351
+ and a production key coexist, and a `GRAILS_API_URL` that something else points at another server finds no key: a
352
+ stored key is never offered to an address you did not connect it to.
353
+ - `disconnect` removes the entry. It does not revoke the key: that is done in Team Settings.
354
+
227
355
  `GRAILS_API_URL` points the CLI at another API (default `https://api.grails.design`; the docs' `…/v1` form is
228
356
  accepted). `http://` is accepted only for this machine, and a redirect is never followed, so the key cannot be sent
229
357
  to an address nobody chose.
@@ -233,6 +361,24 @@ rate limits but does not block yet, so no real `429` exists today: that retry is
233
361
  behaviour. Opening a sync is retried only when the connection was *refused*: if the server may have received it, a
234
362
  second open would be answered "another sync is running" and the first would hold the source for two hours.
235
363
 
364
+ ## Updates
365
+
366
+ Once a day, `grails-cli` asks the npm registry whether a newer version exists: one plain
367
+ `GET https://registry.npmjs.org/grails-cli/latest`, with a 1.5 second limit, no retry, no header of its own, no query,
368
+ and nothing that says who is asking (not the key, the team, the project or the repository). It never fails or delays a
369
+ command beyond that limit; offline is silence. The answer is remembered in `update-check.json` in the config folder
370
+ (`$XDG_CONFIG_HOME/grails-cli`, else `~/.config/grails-cli`; `%APPDATA%\grails-cli` on Windows), a file with no secret in it.
371
+
372
+ While the installed version is behind, **every run** ends with one line on stderr that names both versions and the
373
+ command. CI included: a workflow that pins a version is where nobody sees a release go by. A comparison follows
374
+ semver, so `0.5.0-rc.1` is behind `0.5.0`. `GRAILS_CLI_NO_UPDATE_CHECK=1` turns it off.
375
+
376
+ `grails-cli update [root]` asks now, ignoring the cache, and prints the command for the project's package manager,
377
+ read from the lockfile in the root: `pnpm-lock.yaml` is `pnpm add -D grails-cli@latest`, `yarn.lock` is
378
+ `yarn add -D grails-cli@latest`, anything else is `npm i -D grails-cli@latest`; a project that does not depend on
379
+ `grails-cli` gets `npx grails-cli@latest`. **It does not run it**: the CLI never changes your repository on its own.
380
+ It exits 0 whether you are behind or not, and 4 when the registry cannot be reached.
381
+
236
382
  ## What leaves the repository
237
383
 
238
384
  An allow-list, built field by field (never a copy with fields deleted), enforced again by the