@parity-app/cli 0.1.0 → 0.2.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 +87 -1
- package/dist/bin.mjs +1132 -272
- package/dist/client-Cf66-n3R.mjs +724 -0
- package/dist/index.d.mts +211 -17
- package/dist/index.mjs +1 -1
- package/dist/libraries/aria.lib.par +5 -5
- package/dist/libraries/baseui.lib.par +25 -25
- package/dist/libraries/chakra.lib.par +17 -17
- package/dist/libraries/fluent.lib.par +27 -27
- package/dist/libraries/luma.lib.par +20 -20
- package/dist/libraries/lyra.lib.par +20 -20
- package/dist/libraries/maia.lib.par +20 -20
- package/dist/libraries/mantine.lib.par +14 -14
- package/dist/libraries/mira.lib.par +20 -20
- package/dist/libraries/mui.lib.par +5 -5
- package/dist/libraries/nova.lib.par +20 -20
- package/dist/libraries/radix.lib.par +11 -11
- package/dist/libraries/rhea.lib.par +20 -20
- package/dist/libraries/sera.lib.par +20 -20
- package/dist/libraries/vega.lib.par +20 -20
- package/dist/{src-DYi23Ypb.mjs → src-BOUQtcWB.mjs} +1 -1
- package/dist/{src-FF8eY6EI.mjs → src-BRgxMRtE.mjs} +3 -2
- package/dist/{src-D9EMN48r.mjs → src-BZ1p_ip_.mjs} +125 -36
- package/dist/{src-DXZqtm6z.mjs → src-CAm6EBIK.mjs} +1 -1
- package/dist/{src-8MhDV_sA.mjs → src-CcWc-KsX.mjs} +498 -207
- package/package.json +8 -6
- package/skills/SKILL.md +61 -14
- package/skills/document.md +2 -1
- package/skills/execute.md +74 -8
- package/skills/guide/states-and-modes.md +5 -3
- package/LICENSE +0 -21
- package/dist/client-DsTLQ2lT.mjs +0 -286
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity-app/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Drive the Parity app from a terminal or an agent: read state, run edits, take screenshots, serve MCP.",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"dist",
|
|
35
35
|
"skills"
|
|
36
36
|
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "vp pack",
|
|
39
|
+
"prepack": "vp pack",
|
|
40
|
+
"try-packed": "node scripts/try-packed.ts"
|
|
41
|
+
},
|
|
37
42
|
"publishConfig": {
|
|
38
43
|
"access": "public"
|
|
39
44
|
},
|
|
@@ -42,12 +47,9 @@
|
|
|
42
47
|
"typescript": "6.0.3"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
50
|
+
"@parity-app/agent": "0.0.0",
|
|
45
51
|
"@parity-app/core": "0.0.0",
|
|
46
52
|
"@parity-app/libgen": "0.0.0",
|
|
47
53
|
"@parity-app/registry": "0.0.0"
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "vp pack",
|
|
51
|
-
"try-packed": "node scripts/try-packed.ts"
|
|
52
54
|
}
|
|
53
|
-
}
|
|
55
|
+
}
|
package/skills/SKILL.md
CHANGED
|
@@ -22,25 +22,62 @@ it.
|
|
|
22
22
|
|
|
23
23
|
## Tools
|
|
24
24
|
|
|
25
|
-
The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project
|
|
26
|
-
|
|
25
|
+
The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project. Without
|
|
26
|
+
`tab`, they act on the open project your working folder is in, and otherwise on `active`: what the person is looking at,
|
|
27
|
+
the active tab of the Parity window they used last. When the person says "this" or "what I've selected" without naming a
|
|
28
|
+
project, pass `tab: "active"` (`--tab active`) and read `get_app_state` for the selection.
|
|
27
29
|
|
|
28
30
|
| MCP | CLI | What it does |
|
|
29
31
|
| --------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
32
|
| `get_app_state` | `parity state` | Designs, the selection, modes, components and token names, and what Parity detected (builder and config, package manager, installed versions). Start here. |
|
|
31
|
-
| `
|
|
33
|
+
| `get_status` | `parity status` | The revision, whether anything is unsaved and which files and tokens, the last save, and undo and redo; `summary` names them. |
|
|
34
|
+
| `get_modes` | `parity modes [design]` | Viewports (the ids `Responsive` takes), modes (the ids renders take) and what each design shows; a design adds its frames. |
|
|
35
|
+
| `execute` | `parity exec` | Runs a JavaScript snippet against the designs. Its changes land as one undo step, merged per design with edits made meanwhile. |
|
|
32
36
|
| `screenshot` | `parity screenshot <design>` | Renders a design in its states and modes, offscreen, and returns the images. |
|
|
33
37
|
| `find_problems` | `parity problems <design>` | Render errors, overflow, clipping and unknown components, per frame. |
|
|
34
38
|
| `check_accessibility` | `parity a11y <design>` | axe-core against WCAG 2.2 AA, per frame, on the layers that break each rule. |
|
|
35
39
|
| `get_console` | `parity console` | What the project's own code wrote to the console, on the canvas and offscreen. |
|
|
40
|
+
| `get_tokens` | `parity tokens` | Tokens filtered by category, source or name, with values in every theme. |
|
|
41
|
+
| `get_components` | `parity components` | Components, libraries, icon sets and exports left out or failing to render. |
|
|
42
|
+
| `get_props` | `parity props <Component>` | Props from source; `all` includes passed-through attributes. |
|
|
36
43
|
| `get_tabs` | `parity tabs` | The open projects. |
|
|
37
44
|
| `open_project` | `parity open <folder>` | Opens a project in a new tab. |
|
|
38
45
|
| `read_skill` | `parity skill [file]` | This skill. |
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
The token, component and prop tools read what designs can use without writing a snippet in either transport.
|
|
48
|
+
`parity components` also says which of the app's exports aren't components and why, which components fail to draw on the
|
|
49
|
+
canvas and why (often a missing provider, which a decorator adds), and what the person listed or took off the list by
|
|
50
|
+
hand.
|
|
51
|
+
|
|
52
|
+
CLI commands and MCP tools share validation and defaults. Screenshot scale defaults to 1 and accepts 0.25 to 3. Audits
|
|
53
|
+
check every state and shown mode unless narrowed; a single-element selector list is equivalent to a string. Screenshot
|
|
54
|
+
failures and audit findings exit with 1 in the CLI and set MCP `isError`, with details retained. `get_console` defaults
|
|
55
|
+
`clear` to false; `get_props` defaults `all` to false. MCP project paths must be absolute; the CLI resolves relative
|
|
56
|
+
folders from its working directory. Skill reading is local and needs no running app.
|
|
57
|
+
|
|
58
|
+
Several copies of Parity can run at once, each with its own projects open. Each call goes to the copy that has its
|
|
59
|
+
project open, so naming the project is enough; `get_tabs` lists every copy's projects. A project open in two copies goes
|
|
60
|
+
to the one that has been running longest. A copy kept apart with `PARITY_AGENT_FILE` is reached only through that file:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
parity tabs # every project open in any copy
|
|
64
|
+
parity state --tab active # what the person is looking at, and their selection
|
|
65
|
+
PARITY_AGENT_FILE=copy/agent.json pnpm dev & # a copy of Parity kept apart
|
|
66
|
+
PARITY_AGENT_FILE=copy/agent.json parity tabs # just that copy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Every tool returns its result as data too: MCP `structuredContent`, or `--json` in the CLI. Read that rather than the
|
|
70
|
+
text. A failure has `error.code` to branch on, `retryable` (send it again unchanged), `path` (the input at fault) and
|
|
71
|
+
`alternatives` (valid ids for one it didn't know), so a mistyped id is fixed in one retry:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
parity problems button --state loading --json
|
|
75
|
+
# {"error": {"code": "unknown_state", "path": "/state", "alternatives": ["default", "outline", "destructive", "disabled"], ...}}
|
|
76
|
+
parity problems button --state disabled --json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`get_tokens` returns the first 100 tokens and `get_console` the newest 50 messages, with `total` and `truncated`; narrow
|
|
80
|
+
the read or raise `limit` when `truncated` is true.
|
|
44
81
|
|
|
45
82
|
When a library's components, tokens or icons don't show, `parity lib <package> --root <project>` describes the package
|
|
46
83
|
in a `.lib.par`: its components, tokens and the providers its frames need. It writes `./<id>.lib.par` in the current
|
|
@@ -54,7 +91,8 @@ keeping the designs there, so ask before running that in someone's project.
|
|
|
54
91
|
2. **Look before you change.** Read the design with `Get("design-id", { depth: 3 })` in `execute`, and take a screenshot
|
|
55
92
|
of what's there. Find the components you need with `Components()` and their props with `Props()`.
|
|
56
93
|
3. **Change it in one snippet.** Build or edit the design with `Insert`, `Update`, `Replace`, `Move` and the rest. A
|
|
57
|
-
snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make.
|
|
94
|
+
snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make. If it
|
|
95
|
+
fails because the person changed a design it changes, run it again by its `editId`.
|
|
58
96
|
4. **Cover every state.** A screen that loads has a loading state, a list has an empty one, a form has an error one. Add
|
|
59
97
|
them with `AddState`, as changes to the base design, not as copies.
|
|
60
98
|
5. **Check every frame.** Screenshot the design in all its states and the modes it shows (`state: "all"`,
|
|
@@ -65,7 +103,15 @@ keeping the designs there, so ask before running that in someone's project.
|
|
|
65
103
|
|
|
66
104
|
Your changes to designs are on the canvas but not saved: the person saves (Ctrl+S) or undoes them, as with their own
|
|
67
105
|
edits. Don't say they're saved. Token values from `SetTokens` are the same: they show at once, and go into the project's
|
|
68
|
-
stylesheet when the person saves.
|
|
106
|
+
stylesheet when the person saves. `get_status` says what's saved and what isn't, and its `revision` (also in
|
|
107
|
+
`get_app_state`) stays the same until something changes, so compare it before reading the board again:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
parity status --summary
|
|
111
|
+
# shadcn-vite is at revision 7c612c04:1.
|
|
112
|
+
# 1 file isn't saved. They wait for the person to save; agents can't.
|
|
113
|
+
# shadcn-vite.par: changed button
|
|
114
|
+
```
|
|
69
115
|
|
|
70
116
|
## Rules
|
|
71
117
|
|
|
@@ -86,11 +132,12 @@ stylesheet when the person saves.
|
|
|
86
132
|
- **Reuse what repeats.** When a row, card or list item appears more than twice, make it a component design with
|
|
87
133
|
`ExtractComponent`, so each copy is an instance with its own content.
|
|
88
134
|
- **Leave the canvas tidy.** New designs go into empty space (`FindEmptySpace`); `Overlaps()` tells you if boards
|
|
89
|
-
collide.
|
|
135
|
+
collide, sizing boards the canvas hasn't drawn yet from their frames (`estimated: true`).
|
|
90
136
|
|
|
91
137
|
## When something fails
|
|
92
138
|
|
|
93
|
-
`execute` returns an `editId` when a snippet throws, and nothing changes
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
`execute` returns an `editId` when a snippet throws, and nothing changes; `error.code` says why, and
|
|
140
|
+
`error.alternatives` names the valid ids when a design, state, mode or component wasn't found. Fix it with `editId` and
|
|
141
|
+
`edits` (exact find and replace on the snippet) instead of sending it all again. The error names the line. Common
|
|
142
|
+
causes: a node path that matches two designs (write it as `design/node`), a node property that doesn't exist, and a
|
|
143
|
+
state change to something states can't change (only `hidden`, `text`, `props` and `style`).
|
package/skills/document.md
CHANGED
|
@@ -154,7 +154,8 @@ or hides the node. `ExtractComponent` sets all of this up from the occurrences i
|
|
|
154
154
|
A **variant** is a param whose `options` each patch the component design, the way a state does: a size or a tone the
|
|
155
155
|
instance chooses. Its `default` is an option's id, else the first option. An instance picks one in its `args`, and
|
|
156
156
|
`view.variants` picks which one the component's own board shows. Patches apply in this order: the viewport's, the
|
|
157
|
-
variant options', the state's, then the instance's overrides.
|
|
157
|
+
variant options', the state's, then the instance's overrides. `Get` returns the stored node, with the text it was given;
|
|
158
|
+
`Get(path, { state })` returns it as the board shows it, the param's value filled in and `bind` kept (see execute.md).
|
|
158
159
|
|
|
159
160
|
```json
|
|
160
161
|
"params": {
|
package/skills/execute.md
CHANGED
|
@@ -4,12 +4,30 @@
|
|
|
4
4
|
`await` works at the top level. When the snippet finishes, every change lands on the canvas as one step the person can
|
|
5
5
|
undo; when it throws, nothing changes and you get an `editId` to fix it with.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The person, and other agents, can change the tab while your snippet runs. Only the designs your snippet changed, created
|
|
8
|
+
or deleted are applied, over what's there by then, so their edits to other designs, even in the same file, stay. If a
|
|
9
|
+
design you changed was changed or deleted in the meantime, or the modes were and you set them, nothing is applied and
|
|
10
|
+
the call fails naming it:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
Failed, nothing changed: Not applied: since this call read the tab, "Pricing" (app.par) was changed in the editor, and
|
|
14
|
+
this call changes it too. Run the call again to work from what's there now.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run it again with its `editId` and no edits (`parity exec --edit-id <id>`, or `execute` with just `editId`): the snippet
|
|
18
|
+
reads the tab afresh, so it works from their change. Read the design again first if your snippet depends on what it
|
|
19
|
+
held.
|
|
20
|
+
|
|
21
|
+
- Prefer a `description` of 3 to 8 words, such as "Add a pricing section". CLI `exec` calls it `--label`. Both default
|
|
22
|
+
to "Edit designs" if omitted.
|
|
8
23
|
- `Print(...)` (or `console.log`) is how you see values. Objects print as JSON.
|
|
9
24
|
- Values assigned without `const` or `let` carry over to your next snippet in the same tab.
|
|
10
25
|
- A snippet has 5 seconds of computing time. Rendering (screenshots and checks) doesn't count against it.
|
|
11
26
|
- To retry a failed snippet, send `editId` with `edits`, each an exact `{ find, replace }` on the snippet text (`all` to
|
|
12
|
-
replace every match).
|
|
27
|
+
replace every match). Omitted edits default to an empty list. Do not send a new snippet with `editId`.
|
|
28
|
+
- Both CLI and MCP validate the same request: a nonblank snippet or `editId`, nonblank label and tab when supplied, and
|
|
29
|
+
patches with a nonempty `find`, string `replace` (which can be empty), and optional boolean `all`. Invalid requests
|
|
30
|
+
are rejected before reaching the app.
|
|
13
31
|
|
|
14
32
|
## API
|
|
15
33
|
|
|
@@ -17,14 +35,15 @@ undo; when it throws, nothing changes and you get an `editId` to fix it with.
|
|
|
17
35
|
// Reading
|
|
18
36
|
Designs(): DesignSummary[] // every design: id, name, kind, states, what it shows, size
|
|
19
37
|
GetDesign(design): Design // a whole design, as in document.md
|
|
20
|
-
Get(path, { depth?, state? }?): Node
|
|
38
|
+
Get(path, { depth?, state?, mode? }?): Node // a design's root or a node; depth cuts children off with "…"
|
|
21
39
|
Get(path?, visit): any[] // walks nodes, collecting what visit returns
|
|
40
|
+
Get(path, visit, { state?, mode? }): any[] // walks nodes as a frame draws them
|
|
22
41
|
Components(): string[] // the components designs can use
|
|
23
42
|
Props(component, { all? }?): PropsInfo // a component's props, from its source
|
|
24
43
|
Icons(): IconSet[] // the project's icon sets, with their styles
|
|
25
44
|
Icons(set, { search?, style?, limit? }): Icon[] // async: a set's icons, as component nodes name them
|
|
26
45
|
GetTokens({ category?, source?, search? }?): Token[]
|
|
27
|
-
GetModes(): { settings, modes }
|
|
46
|
+
GetModes(design?): { settings, variables, viewports, modes, designs } // modes resolved; what each design shows
|
|
28
47
|
Selection(): { design?, node?, mode? } // what the person has selected
|
|
29
48
|
|
|
30
49
|
// Designs
|
|
@@ -82,11 +101,27 @@ canvas, the main mode and the modes beside it. Start here to find ids.
|
|
|
82
101
|
### Get
|
|
83
102
|
|
|
84
103
|
`Get("dashboard", { depth: 2 })` returns the design's root with two levels of children; deeper ones show as `"…"`. Read
|
|
85
|
-
a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`.
|
|
86
|
-
|
|
104
|
+
a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`. Without `state` or `mode`, Get returns the
|
|
105
|
+
nodes as stored, which is what `Update` and the other edits change.
|
|
106
|
+
|
|
107
|
+
With `state`, `mode` or both, Get returns the nodes as that frame of the board draws them: the design's variant options
|
|
108
|
+
(`view.variants`), its params' defaults filled into bound values, its responsive patches for the mode, then the state's
|
|
109
|
+
patches. `mode` is a mode id from `GetModes()`, as Screenshot takes. A state alone is read in the design's main mode,
|
|
110
|
+
and a mode alone in the state the design shows. A bound value comes back filled in, with its `bind` kept so you can tell
|
|
111
|
+
it comes from a param. An instance comes back as its own node with the args it's drawn with; read what's inside it by
|
|
112
|
+
its path, `design/instance/inner`, which only works with `state` or `mode`.
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// A component whose title is bound to its "title" param: stored, then as the board shows it.
|
|
116
|
+
Print(Get("card/title").text) // "Title"
|
|
117
|
+
Print(Get("card/title", { state: "base" }).text) // "Card title", the param's default
|
|
118
|
+
// A copy of the card on a screen, inside the instance, in a state and on mobile.
|
|
119
|
+
Print(Get("profile/person/title", { state: "selected", mode: "mobile-light" }).text) // "Selected"
|
|
120
|
+
```
|
|
87
121
|
|
|
88
122
|
`Get(path, visit)` walks the nodes under `path`, or every design without one, and returns what `visit` returns for each.
|
|
89
|
-
`visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing.
|
|
123
|
+
`visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing. Given a
|
|
124
|
+
path, `Get(path, visit, { state, mode })` visits the nodes as that frame draws them, as above.
|
|
90
125
|
|
|
91
126
|
```js
|
|
92
127
|
// Every Button in the project, with where it is.
|
|
@@ -239,6 +274,18 @@ ExtractComponent("team-members", rows.nodes, { name: "Member row", params: { [ro
|
|
|
239
274
|
`SetModes(settings)` replaces the project's modes: take `GetModes().settings`, change it, and pass it back. Modes are
|
|
240
275
|
shared by every design.
|
|
241
276
|
|
|
277
|
+
`GetModes()` also gives `viewports`, the ids `Responsive` takes, each with its width and window height; `modes`, the ids
|
|
278
|
+
`Screenshot` and the checks take, each with its `values` (one option per variable), width, window height, classes and
|
|
279
|
+
attributes; and `designs`, each design's `state` (or `states` when it shows every state), `main` mode and `beside`
|
|
280
|
+
modes. `GetModes("dashboard")` gives just that design, with each frame's width, fixed `height` if it has one, and
|
|
281
|
+
`window`.
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const { viewports, designs } = GetModes("dashboard")
|
|
285
|
+
Print(viewports.map((viewport) => viewport.id)) // ["desktop", "tablet", "mobile"]: for Responsive
|
|
286
|
+
Print(designs[0].frames.map((frame) => `${frame.mode} ${frame.window.width}x${frame.window.height}`))
|
|
287
|
+
```
|
|
288
|
+
|
|
242
289
|
`SetTokens` changes token values in the project's stylesheets: `SetTokens({ primary: "oklch(0.55 0.2 260)" })` for every
|
|
243
290
|
theme, or `SetTokens({ primary: { default: "…", dark: "…" } })` per theme. Every frame shows the new values at once, and
|
|
244
291
|
saving writes them into the project's code, so do it only when the person asks to change the design system, not to style
|
|
@@ -250,15 +297,34 @@ one design. They're undone with the call's other changes.
|
|
|
250
297
|
result. `state` and `mode` take an id, a list, or `"all"`; `mode` also takes `"shown"`, the modes the design shows. The
|
|
251
298
|
defaults are the state on the canvas and the main mode.
|
|
252
299
|
|
|
300
|
+
Frames draw as the canvas does: instances of a library's component designs (from its `.lib.par`) draw that design, and
|
|
301
|
+
token values not yet saved show, the person's edits and this call's own `SetTokens` alike, so
|
|
302
|
+
`SetTokens({ primary: "red" }); await Screenshot(id)` shows the new value in the same call.
|
|
303
|
+
|
|
253
304
|
`Measure` lays out one frame and returns its size, whether it overflows sideways, and every node's box (`clipped` when a
|
|
254
305
|
parent cuts it off). `Problems` checks frames for render errors, overflow, clipping and unknown components.
|
|
255
306
|
`Accessibility` returns axe-core's findings per node and frame, with the rule, its impact and what's wrong.
|
|
256
307
|
|
|
308
|
+
These frames are drawn in the same window as the canvas's: as wide as the frame, and as tall as the design's fixed
|
|
309
|
+
height for the viewport, else the viewport's screen height, else the device nearest its width. So `100vh` and window
|
|
310
|
+
size checks measure the same in `Measure` and `Screenshot` as on the canvas, even when the frame grows past that height.
|
|
311
|
+
|
|
257
312
|
## The canvas
|
|
258
313
|
|
|
259
314
|
`FindEmptySpace({ width, height })` finds room for a new board, to the right of the others unless `direction: "below"`,
|
|
260
315
|
or next to the design named in `near`. `Overlaps()` lists boards that collide, titles included; move one with
|
|
261
|
-
`UpdateDesign(design, { x, y })`.
|
|
316
|
+
`UpdateDesign(design, { x, y })`. A board is as big as the canvas last drew it. One it hasn't drawn yet is sized from
|
|
317
|
+
its frames, laid out as the canvas lays them out: each shown mode's frame size (`frames` on the design, else the mode's
|
|
318
|
+
width), companions shrunk to 640 wide, and a frame without a fixed height as tall as its window. Those overlaps say
|
|
319
|
+
`estimated: true`.
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// With the default modes: a 1280 main frame, the dark one beside it at half size and the 390 phone, 2505 wide in all.
|
|
323
|
+
const dashboard = CreateDesign({ name: "Dashboard", x: 0, y: 0 })
|
|
324
|
+
UpdateDesign(dashboard, { showModes: ["desktop-dark", "mobile-light"] })
|
|
325
|
+
CreateDesign({ name: "Settings", x: 2200, y: 0 })
|
|
326
|
+
Print(Overlaps(dashboard)) // Dashboard and Settings, 305 across, estimated: true
|
|
327
|
+
```
|
|
262
328
|
|
|
263
329
|
## Examples
|
|
264
330
|
|
|
@@ -30,9 +30,11 @@ Modes are the project's, shared by every design. Each mode picks one option of e
|
|
|
30
30
|
- **Theme:** a class on the frame, such as `dark`, that switches the project's tokens.
|
|
31
31
|
- **Locale:** `lang` and `dir` attributes, for languages and right-to-left scripts.
|
|
32
32
|
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
`get_modes` (`parity modes`) lists them, and `get_modes` with a design gives its frames: run it before a responsive
|
|
34
|
+
change, since the ids `Responsive` takes are viewports (`mobile`) and the ids screenshots take are modes
|
|
35
|
+
(`mobile-light`). `GetModes()` gives the same in a snippet. A design shows its main mode and, beside it, any others you
|
|
36
|
+
choose with `UpdateDesign(design, { showModes: ["desktop-dark", "mobile-light"] })`. Show the modes that matter for the
|
|
37
|
+
design: a dashboard in desktop and mobile, a component in light and dark.
|
|
36
38
|
|
|
37
39
|
## Viewports and responsive patches
|
|
38
40
|
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Andrew Craswell
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/client-DsTLQ2lT.mjs
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
// src/caller.ts
|
|
6
|
-
/** Who is driving Parity, named the way people know it, for the agent's cursor on the canvas. */
|
|
7
|
-
/** Known clients, by the name they give MCP's `initialize`, most specific first. */
|
|
8
|
-
const clients = [
|
|
9
|
-
[/claude[-_ ]?code/i, "Claude Code"],
|
|
10
|
-
[/^claude/i, "Claude"],
|
|
11
|
-
[/cursor/i, "Cursor"],
|
|
12
|
-
[/codex/i, "Codex"],
|
|
13
|
-
[/gemini/i, "Gemini CLI"],
|
|
14
|
-
[/windsurf|codeium/i, "Windsurf"],
|
|
15
|
-
[/copilot|visual studio code|^vscode/i, "Copilot"],
|
|
16
|
-
[/zed/i, "Zed"],
|
|
17
|
-
[/cline/i, "Cline"],
|
|
18
|
-
[/goose/i, "Goose"]
|
|
19
|
-
];
|
|
20
|
-
/**
|
|
21
|
-
* An MCP client's name for people: a known agent's own name, else the title it gives, else its name with its
|
|
22
|
-
* separators made spaces and its words capitalized ("my-agent" becomes "My Agent").
|
|
23
|
-
*/
|
|
24
|
-
function mcpCallerName(clientInfo) {
|
|
25
|
-
const name = typeof clientInfo?.name === "string" ? clientInfo.name.trim() : "";
|
|
26
|
-
const title = typeof clientInfo?.title === "string" ? clientInfo.title.trim() : "";
|
|
27
|
-
for (const [pattern, known] of clients) if (pattern.test(name) || pattern.test(title)) return known;
|
|
28
|
-
if (title) return title;
|
|
29
|
-
if (!name) return void 0;
|
|
30
|
-
return name.replace(/[-_]+(mcp[-_]?)?client$/i, "").split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Who runs the CLI, from what agents put in their shell's environment. `PARITY_AGENT_NAME` names it outright; with
|
|
34
|
-
* no sign of an agent, it's someone at a terminal.
|
|
35
|
-
*/
|
|
36
|
-
function cliCallerName(env) {
|
|
37
|
-
const named = env.PARITY_AGENT_NAME?.trim();
|
|
38
|
-
if (named) return named;
|
|
39
|
-
if (env.CLAUDECODE === "1") return "Claude Code";
|
|
40
|
-
if (env.GEMINI_CLI === "1") return "Gemini CLI";
|
|
41
|
-
if (Object.keys(env).some((key) => key.startsWith("CODEX_"))) return "Codex";
|
|
42
|
-
return "Terminal";
|
|
43
|
-
}
|
|
44
|
-
// src/client.ts
|
|
45
|
-
/** Where the running app writes its address and token. */
|
|
46
|
-
const agentFile = process.env.PARITY_AGENT_FILE ?? join(homedir(), ".parity", "agent.json");
|
|
47
|
-
let caller = cliCallerName(process.env);
|
|
48
|
-
/** Tells callers with the same name apart. A terminal's calls are separate processes, so they share their name. */
|
|
49
|
-
let callerId = process.env.PARITY_AGENT_ID?.trim() || caller;
|
|
50
|
-
/**
|
|
51
|
-
* Names who's calling in later calls, so the canvas can say who's at work. The MCP server learns the name from its
|
|
52
|
-
* client, and is one session: its calls share an id, apart from any other session of the same agent.
|
|
53
|
-
*/
|
|
54
|
-
function setCaller(name, session = false) {
|
|
55
|
-
caller = name ?? caller;
|
|
56
|
-
callerId = session ? `${caller}#${randomUUID().slice(0, 8)}` : caller;
|
|
57
|
-
}
|
|
58
|
-
/** Calls the running Parity app. Fails with a clear message when it isn't running. */
|
|
59
|
-
async function call(method, params = {}) {
|
|
60
|
-
let connection;
|
|
61
|
-
try {
|
|
62
|
-
connection = JSON.parse(await readFile(agentFile, "utf8"));
|
|
63
|
-
} catch {
|
|
64
|
-
throw new Error(`Parity isn't running (no ${agentFile}). Start the app, then try again.`);
|
|
65
|
-
}
|
|
66
|
-
let response;
|
|
67
|
-
try {
|
|
68
|
-
response = await fetch(`${connection.url}/rpc`, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
headers: {
|
|
71
|
-
"content-type": "application/json",
|
|
72
|
-
authorization: `Bearer ${connection.token}`
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
method,
|
|
76
|
-
params,
|
|
77
|
-
agent: caller,
|
|
78
|
-
agentId: callerId
|
|
79
|
-
})
|
|
80
|
-
});
|
|
81
|
-
} catch {
|
|
82
|
-
throw new Error(`Parity isn't answering at ${connection.url}. Is the app still running?`);
|
|
83
|
-
}
|
|
84
|
-
const body = await response.json();
|
|
85
|
-
if (body.error !== void 0) throw new Error(body.error);
|
|
86
|
-
return body.result;
|
|
87
|
-
}
|
|
88
|
-
/** A readable account of an exec call, for terminals and for agents. */
|
|
89
|
-
function describeExec(result) {
|
|
90
|
-
const lines = [];
|
|
91
|
-
if (result.ok) lines.push(result.applied ? "Done. The changes are on the canvas as one undoable step." : "Done. Nothing changed.");
|
|
92
|
-
else {
|
|
93
|
-
lines.push(`Failed, nothing changed: ${result.error}`);
|
|
94
|
-
lines.push(`Fix it by retrying with editId "${result.editId}" and edits ({find, replace}) to the snippet.`);
|
|
95
|
-
}
|
|
96
|
-
if (result.output.length > 0) lines.push("", "Output:", ...result.output);
|
|
97
|
-
const created = Object.entries(result.created);
|
|
98
|
-
if (created.length > 0) lines.push("", "Created:", ...created.map(([name, id]) => ` ${name} = ${id}`));
|
|
99
|
-
if (result.warnings.length > 0) lines.push("", "Warnings (fix these next):", ...result.warnings.map((warning) => ` ${warning}`));
|
|
100
|
-
if (result.screenshots.length > 0) lines.push("", "Screenshots:", ...result.screenshots.map((shot) => ` ${shot.design} / ${shot.state} / ${shot.mode}: ${shot.path}`));
|
|
101
|
-
return lines.join("\n");
|
|
102
|
-
}
|
|
103
|
-
const impactOrder = [
|
|
104
|
-
"critical",
|
|
105
|
-
"serious",
|
|
106
|
-
"moderate",
|
|
107
|
-
"minor"
|
|
108
|
-
];
|
|
109
|
-
/**
|
|
110
|
-
* A readable account of an accessibility check: each rule broken, worst first, with the layers that break it, the
|
|
111
|
-
* frames they break it in and what's wrong. Alike layers, such as the rows of a list, share a line.
|
|
112
|
-
*/
|
|
113
|
-
function describeAccessibility(result) {
|
|
114
|
-
const { design, findings, frames } = result;
|
|
115
|
-
const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
|
|
116
|
-
const rules = /* @__PURE__ */ new Map();
|
|
117
|
-
for (const finding of findings) rules.set(finding.rule, [...rules.get(finding.rule) ?? [], finding]);
|
|
118
|
-
const lines = [rules.size === 0 ? `No issues found in ${checked}, with axe-core against WCAG 2.2 AA.` : `${rules.size === 1 ? "1 rule" : `${rules.size} rules`} broken in ${checked}, with axe-core against WCAG 2.2 AA.`];
|
|
119
|
-
const groups = [...rules.values()].sort((a, b) => impactOrder.indexOf(a[0].impact) - impactOrder.indexOf(b[0].impact));
|
|
120
|
-
for (const group of groups) {
|
|
121
|
-
const { impact, rule, help, helpUrl } = group[0];
|
|
122
|
-
lines.push("", `${impact}: ${help} (${rule})`);
|
|
123
|
-
const byLayer = /* @__PURE__ */ new Map();
|
|
124
|
-
for (const finding of group) {
|
|
125
|
-
const key = `${finding.node ?? ""}\n${finding.detail}`;
|
|
126
|
-
const layer = byLayer.get(key) ?? {
|
|
127
|
-
name: finding.name ?? "The design",
|
|
128
|
-
nodes: finding.node ? [finding.node] : [],
|
|
129
|
-
detail: finding.detail,
|
|
130
|
-
frames: []
|
|
131
|
-
};
|
|
132
|
-
layer.frames.push({
|
|
133
|
-
state: finding.state,
|
|
134
|
-
mode: finding.mode
|
|
135
|
-
});
|
|
136
|
-
byLayer.set(key, layer);
|
|
137
|
-
}
|
|
138
|
-
const alike = /* @__PURE__ */ new Map();
|
|
139
|
-
for (const layer of byLayer.values()) {
|
|
140
|
-
const same = JSON.stringify([
|
|
141
|
-
layer.name,
|
|
142
|
-
layer.detail,
|
|
143
|
-
layer.frames
|
|
144
|
-
]);
|
|
145
|
-
const existing = alike.get(same);
|
|
146
|
-
if (existing) existing.nodes.push(...layer.nodes);
|
|
147
|
-
else alike.set(same, layer);
|
|
148
|
-
}
|
|
149
|
-
for (const layer of alike.values()) {
|
|
150
|
-
const where = layer.frames.length === frames && frames > 1 ? "every frame" : frameList(layer.frames);
|
|
151
|
-
const ids = layer.nodes.length > 0 ? ` (${layer.nodes.join(", ")})` : "";
|
|
152
|
-
lines.push(` ${layer.name}${ids}, in ${where}: ${layer.detail}`);
|
|
153
|
-
}
|
|
154
|
-
lines.push(` How to fix it: ${helpUrl}`);
|
|
155
|
-
}
|
|
156
|
-
if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
|
|
157
|
-
lines.push("", "Automated checks find some issues, not all. Try the design with a keyboard and a screen reader too.");
|
|
158
|
-
return lines.join("\n");
|
|
159
|
-
}
|
|
160
|
-
/** A readable account of the layout problems in a design's frames, each once with the frames it's in. */
|
|
161
|
-
function describeProblems(result) {
|
|
162
|
-
const { design, problems, frames } = result;
|
|
163
|
-
const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
|
|
164
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
165
|
-
for (const { problem, node, state, mode } of problems) {
|
|
166
|
-
const key = `${node ?? ""}\n${problem}`;
|
|
167
|
-
const entry = grouped.get(key) ?? {
|
|
168
|
-
problem,
|
|
169
|
-
...node ? { node } : {},
|
|
170
|
-
frames: []
|
|
171
|
-
};
|
|
172
|
-
entry.frames.push({
|
|
173
|
-
state,
|
|
174
|
-
mode
|
|
175
|
-
});
|
|
176
|
-
grouped.set(key, entry);
|
|
177
|
-
}
|
|
178
|
-
const count = grouped.size === 1 ? "1 problem" : `${grouped.size} problems`;
|
|
179
|
-
const lines = [grouped.size === 0 ? `No problems found in ${checked}.` : `${count} in ${checked}:`];
|
|
180
|
-
for (const { problem, node, frames: where } of grouped.values()) {
|
|
181
|
-
const every = where.length === frames && frames > 1 ? "every frame" : frameList(where);
|
|
182
|
-
lines.push(` ${node ? `${node}: ` : ""}${problem}, in ${every}`);
|
|
183
|
-
}
|
|
184
|
-
if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
|
|
185
|
-
return lines.join("\n");
|
|
186
|
-
}
|
|
187
|
-
/** Tokens by category, each with its value in every theme that sets one. */
|
|
188
|
-
function describeTokens(tokens) {
|
|
189
|
-
if (tokens.length === 0) return "No tokens match.";
|
|
190
|
-
const lines = [`${tokens.length === 1 ? "1 token" : `${tokens.length} tokens`}. Name them in styles without "--".`];
|
|
191
|
-
const categories = /* @__PURE__ */ new Map();
|
|
192
|
-
for (const token of tokens) {
|
|
193
|
-
const key = `${token.category}, ${token.source}`;
|
|
194
|
-
categories.set(key, [...categories.get(key) ?? [], token]);
|
|
195
|
-
}
|
|
196
|
-
for (const [category, list] of categories) {
|
|
197
|
-
lines.push("", `${category}:`);
|
|
198
|
-
for (const token of list) {
|
|
199
|
-
const values = Object.entries(token.values).sort(([a], [b]) => first("default", a, b)).map(([theme, value]) => `${theme} ${value}`);
|
|
200
|
-
lines.push(` ${token.name}: ${values.join(", ")}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return lines.join("\n");
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* The project's own components, then each library's, then the icon libraries, then the exports left out and why, the
|
|
207
|
-
* components failing to draw and why, and what the person corrected by hand.
|
|
208
|
-
*/
|
|
209
|
-
function describeComponents({ components, iconLibraries = [], leftOut = [], drawn = 0, failing = [], corrections = {} }) {
|
|
210
|
-
const groups = /* @__PURE__ */ new Map();
|
|
211
|
-
for (const component of components) {
|
|
212
|
-
const slash = component.indexOf("/");
|
|
213
|
-
const group = slash > 0 ? component.slice(0, slash) : "";
|
|
214
|
-
groups.set(group, [...groups.get(group) ?? [], slash > 0 ? component.slice(slash + 1) : component]);
|
|
215
|
-
}
|
|
216
|
-
const lines = [];
|
|
217
|
-
for (const [group, names] of [...groups].sort(([a], [b]) => first("", a, b))) lines.push(group ? `${group}, as ${group}/Name:` : "The project's own:", ` ${names.join(", ")}`, "");
|
|
218
|
-
if (iconLibraries.length > 0) lines.push(`Icons: ${iconLibraries.map((library) => `${library}, as ${library}/Name`).join("; ")}`, "");
|
|
219
|
-
if (leftOut.length > 0) {
|
|
220
|
-
lines.push("Not components:");
|
|
221
|
-
for (const entry of leftOut) lines.push(` ${entry.name}, in ${entry.path}: ${entry.undecided ? "can't tell, " : ""}${entry.reason}`);
|
|
222
|
-
lines.push("");
|
|
223
|
-
}
|
|
224
|
-
if (failing.length > 0) {
|
|
225
|
-
lines.push(`Failing to draw on the canvas, ${failing.length} of ${failing.length + drawn} drawn so far:`);
|
|
226
|
-
for (const entry of failing) lines.push(` ${entry.id}: ${entry.reason}`);
|
|
227
|
-
lines.push("");
|
|
228
|
-
}
|
|
229
|
-
const { include = [], exclude = [] } = corrections;
|
|
230
|
-
if (include.length > 0 || exclude.length > 0) {
|
|
231
|
-
const name = (ref) => ref.export === "default" ? "the default export" : ref.export;
|
|
232
|
-
lines.push("Corrected by hand in the project file:");
|
|
233
|
-
for (const ref of include) lines.push(` listed: ${name(ref)}, in ${ref.path}`);
|
|
234
|
-
for (const ref of exclude) lines.push(` taken off: ${name(ref)}, in ${ref.path}`);
|
|
235
|
-
}
|
|
236
|
-
return lines.join("\n").trimEnd();
|
|
237
|
-
}
|
|
238
|
-
/** A component's props, with each variant's options and default. */
|
|
239
|
-
function describeProps(result) {
|
|
240
|
-
if (!result.known) return `${result.component}'s props haven't been read from its source yet. Try again in a moment.`;
|
|
241
|
-
const imported = result.import && result.import !== result.path ? `, imported from "${result.import}"` : "";
|
|
242
|
-
const lines = [`${result.component}, from ${result.path}${imported}:`];
|
|
243
|
-
for (const prop of result.props) {
|
|
244
|
-
const options = prop.options ? `: ${prop.options.map((option) => JSON.stringify(option)).join(", ")}` : "";
|
|
245
|
-
const fallback = prop.default === void 0 ? "" : `, default ${JSON.stringify(prop.default)}`;
|
|
246
|
-
lines.push(` ${prop.name} (${prop.kind}${prop.required ? ", required" : ""})${options}${fallback}`);
|
|
247
|
-
}
|
|
248
|
-
if (result.props.length === 0) lines.push(" No props of its own. --all lists the attributes it passes on.");
|
|
249
|
-
return lines.join("\n");
|
|
250
|
-
}
|
|
251
|
-
/** Frames as state and mode ids, naming a mode once when they share it: "default, error in desktop-light". */
|
|
252
|
-
function frameList(frames) {
|
|
253
|
-
const modes = [...new Set(frames.map((frame) => frame.mode))];
|
|
254
|
-
if (modes.length === 1) return `${frames.map((frame) => frame.state).join(", ")} in ${modes[0]}`;
|
|
255
|
-
return frames.map((frame) => `${frame.state} in ${frame.mode}`).join(", ");
|
|
256
|
-
}
|
|
257
|
-
/** A readable account of what a project's code wrote to the console, oldest first, with where each came from. */
|
|
258
|
-
function describeConsole(result) {
|
|
259
|
-
const { tab, entries } = result;
|
|
260
|
-
if (entries.length === 0) return `${tab.name}'s code hasn't written anything to the console.`;
|
|
261
|
-
const lines = [`${entries.length === 1 ? "1 message" : `${entries.length} messages`} from ${tab.name}'s code, oldest first:`];
|
|
262
|
-
for (const entry of entries) {
|
|
263
|
-
const time = new Date(entry.last).toLocaleTimeString([], { hour12: false });
|
|
264
|
-
const repeats = entry.count > 1 ? ` (${entry.count} times)` : "";
|
|
265
|
-
const [first = "", ...rest] = entry.text.split("\n");
|
|
266
|
-
lines.push("", `${time} ${entry.level}${repeats}: ${first}`, ...rest.map((line) => ` ${line}`));
|
|
267
|
-
const offscreen = entry.where === "offscreen" ? ", in an offscreen render" : "";
|
|
268
|
-
lines.push(` at ${scriptPath(entry.source)}:${entry.line}${offscreen}`);
|
|
269
|
-
}
|
|
270
|
-
return lines.join("\n");
|
|
271
|
-
}
|
|
272
|
-
/** Compares two names alphabetically, with `name` before every other. */
|
|
273
|
-
function first(name, a, b) {
|
|
274
|
-
if (a === name) return -1;
|
|
275
|
-
if (b === name) return 1;
|
|
276
|
-
return a.localeCompare(b);
|
|
277
|
-
}
|
|
278
|
-
/** A script's path on its server, without the cache-busting query Vite adds. */
|
|
279
|
-
function scriptPath(url) {
|
|
280
|
-
try {
|
|
281
|
-
return new URL(url).pathname;
|
|
282
|
-
} catch {
|
|
283
|
-
return url || "an unknown script";
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
export { describeConsole as a, describeProps as c, mcpCallerName as d, describeComponents as i, describeTokens as l, call as n, describeExec as o, describeAccessibility as r, describeProblems as s, agentFile as t, setCaller as u };
|