@weft-js/cli 0.1.0 → 0.1.2
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 +38 -14
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +108 -0
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +174 -188
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +68 -444
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +199 -207
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +228 -224
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +142 -0
- package/src/deploy/files.mjs +266 -0
- package/src/deploy/fit.mjs +166 -0
- package/src/deploy/hosts.mjs +126 -0
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +387 -0
- package/src/deploy/needs.mjs +149 -0
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -42
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +144 -116
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +194 -92
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { expectations, actions as browserActions } from '../browser/commands.mjs'
|
|
2
|
+
|
|
3
|
+
const target = {
|
|
4
|
+
description:
|
|
5
|
+
'What the user sees: { role, name } (name exact), { label }, { text }, or a ref from the latest snapshot ("@e12")',
|
|
6
|
+
anyOf: [
|
|
7
|
+
{ type: 'string', pattern: '^@?e\\d+$' },
|
|
8
|
+
{
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
role: { type: 'string' },
|
|
12
|
+
name: { type: 'string' },
|
|
13
|
+
exact: { type: 'boolean' },
|
|
14
|
+
},
|
|
15
|
+
required: ['role'],
|
|
16
|
+
},
|
|
17
|
+
{ type: 'object', properties: { label: { type: 'string' } }, required: ['label'] },
|
|
18
|
+
{ type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The `weft browser` commands as tools. The page is the app open in any browser; the dev server relays to it. */
|
|
23
|
+
export function browserTools() {
|
|
24
|
+
const tab = {
|
|
25
|
+
url: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Dev server URL (default: .weft/dev.json, then environments.dev.url)',
|
|
28
|
+
},
|
|
29
|
+
tab: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Tab id from browser_tabs (default: the focused or latest tab)',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
const t = (name, description, properties = {}, required) => ({
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: { ...properties, ...tab },
|
|
40
|
+
...(required ? { required } : {}),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
return [
|
|
44
|
+
t(
|
|
45
|
+
'browser_tabs',
|
|
46
|
+
"Tabs of the app connected to the dev server. Any browser works (yours, the person's); with none, call browser_launch. If your own browser can run JS, `await __weft__.agent.run(command, args)` in the page does the same as these tools.",
|
|
47
|
+
),
|
|
48
|
+
t(
|
|
49
|
+
'browser_launch',
|
|
50
|
+
'Start a headless Chrome on the app (only when you have no browser of your own)',
|
|
51
|
+
{ route: { type: 'string' }, headed: { type: 'boolean' } },
|
|
52
|
+
),
|
|
53
|
+
t('browser_open', 'Navigate the tab to a route', { route: { type: 'string' } }, ['route']),
|
|
54
|
+
t(
|
|
55
|
+
'browser_snapshot',
|
|
56
|
+
'The page as roles, names and refs (`- button "Add Banana" [ref=e15]`), plus weft\'s live reactive graph with real names (`Cart.total = 42.5 ← Cart.subtotal → 1 binding in <Cart>`), and errors since the last call. Big payloads come back as a file path.',
|
|
57
|
+
{
|
|
58
|
+
interactive: { type: 'boolean', description: 'Only interactive elements' },
|
|
59
|
+
delta: { type: 'boolean', description: 'Only what changed since the last snapshot' },
|
|
60
|
+
scope: target,
|
|
61
|
+
graph: {
|
|
62
|
+
description: "false: no graph; 'only': only the graph; a string filters it by name",
|
|
63
|
+
anyOf: [{ type: 'boolean' }, { type: 'string' }],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
t(
|
|
68
|
+
'browser_act',
|
|
69
|
+
`Act like a user (user-event, as in @weft-js/core/test) and get back what changed on the page. Actions: ${browserActions.join(', ')}`,
|
|
70
|
+
{
|
|
71
|
+
target,
|
|
72
|
+
action: { type: 'string', enum: browserActions },
|
|
73
|
+
value: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'fill/type text, press key (`{Enter}`), select option',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
['target', 'action'],
|
|
79
|
+
),
|
|
80
|
+
t(
|
|
81
|
+
'browser_assert',
|
|
82
|
+
`Check what the user sees (retries up to timeout ms). Expectations: ${expectations.join(', ')}. Recorded for browser_record.`,
|
|
83
|
+
{
|
|
84
|
+
target,
|
|
85
|
+
expect: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
description:
|
|
88
|
+
'{ text: "Total $4" } | { value: "SAVE10" } | { disabled: true } | { count: 3 } | { absent: true } | …',
|
|
89
|
+
},
|
|
90
|
+
timeout: { type: 'number' },
|
|
91
|
+
},
|
|
92
|
+
['target', 'expect'],
|
|
93
|
+
),
|
|
94
|
+
...browserStateTools(t),
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function browserStateTools(t) {
|
|
99
|
+
return [
|
|
100
|
+
t(
|
|
101
|
+
'browser_state',
|
|
102
|
+
'Read the named reactive graph, or write a box by its graph name to reach a state fast (dev only)',
|
|
103
|
+
{
|
|
104
|
+
filter: { type: 'string', description: 'Name prefix (`Cart`, `cart.`)' },
|
|
105
|
+
set: { type: 'string', description: 'Graph name to write, e.g. Cart.coupon' },
|
|
106
|
+
value: { description: 'JSON value to write' },
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
t(
|
|
110
|
+
'browser_time',
|
|
111
|
+
"The page's clock: pause (reload: from page load, so startup timers are faked too), advance ms (runs due timers in order), resume, status",
|
|
112
|
+
{
|
|
113
|
+
op: { type: 'string', enum: ['pause', 'advance', 'resume', 'status'] },
|
|
114
|
+
ms: { type: 'number' },
|
|
115
|
+
reload: { type: 'boolean' },
|
|
116
|
+
},
|
|
117
|
+
['op'],
|
|
118
|
+
),
|
|
119
|
+
t(
|
|
120
|
+
'browser_net',
|
|
121
|
+
'The network as the app sees it: offline (navigator.onLine false, offline event, fetch fails, sockets dropped), online, latency ms (fetch), drop (close open WebSockets/EventSources: a server restart), reset, status',
|
|
122
|
+
{
|
|
123
|
+
op: { type: 'string', enum: ['offline', 'online', 'latency', 'drop', 'reset', 'status'] },
|
|
124
|
+
ms: { type: 'number' },
|
|
125
|
+
},
|
|
126
|
+
['op'],
|
|
127
|
+
),
|
|
128
|
+
t(
|
|
129
|
+
'browser_record',
|
|
130
|
+
'Record what happens in the tab (your acts and asserts, and clicks/typing from any browser tool) and turn it into a @weft-js/core/test test, which then has to pass the gate',
|
|
131
|
+
{
|
|
132
|
+
op: { type: 'string', enum: ['start', 'stop', 'steps', 'note', 'code'] },
|
|
133
|
+
text: { type: 'string' },
|
|
134
|
+
component: { type: 'string' },
|
|
135
|
+
from: { type: 'string' },
|
|
136
|
+
props: { type: 'string' },
|
|
137
|
+
name: { type: 'string' },
|
|
138
|
+
out: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Write the test to this file (relative to the project)',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
['op'],
|
|
144
|
+
),
|
|
145
|
+
t(
|
|
146
|
+
'browser_logs',
|
|
147
|
+
'Console errors and warnings, and uncaught errors with the weft binding they came from, since the last call',
|
|
148
|
+
),
|
|
149
|
+
t(
|
|
150
|
+
'browser_screenshot',
|
|
151
|
+
'PNG of the launched Chrome (browser_launch); in your own browser, use its screenshot tool',
|
|
152
|
+
{ out: { type: 'string' } },
|
|
153
|
+
),
|
|
154
|
+
]
|
|
155
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -11,94 +11,132 @@ import { docs, topics } from './docs.mjs'
|
|
|
11
11
|
import { listSurfaces } from './surface-checks.mjs'
|
|
12
12
|
import { routesCommand } from './start.mjs'
|
|
13
13
|
import { tasks } from './tasks.mjs'
|
|
14
|
-
import { browser
|
|
14
|
+
import { browser } from './browser/commands.mjs'
|
|
15
|
+
import { browserTools } from './mcp/browser-tools.mjs'
|
|
15
16
|
import { gate } from './gate.mjs'
|
|
16
17
|
import { explainTarget, findGroups, styleMatrix } from './style.mjs'
|
|
17
18
|
|
|
18
19
|
const versions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']
|
|
19
20
|
|
|
20
|
-
const target = {
|
|
21
|
-
description: 'What the user sees: { role, name } (name exact), { label }, { text }, or a ref from the latest snapshot ("@e12")',
|
|
22
|
-
anyOf: [
|
|
23
|
-
{ type: 'string', pattern: '^@?e\\d+$' },
|
|
24
|
-
{ type: 'object', properties: { role: { type: 'string' }, name: { type: 'string' }, exact: { type: 'boolean' } }, required: ['role'] },
|
|
25
|
-
{ type: 'object', properties: { label: { type: 'string' } }, required: ['label'] },
|
|
26
|
-
{ type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
|
|
27
|
-
],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The `weft browser` commands as tools. The page is the app open in any browser; the dev server relays to it. */
|
|
31
|
-
function browserTools() {
|
|
32
|
-
const tab = { url: { type: 'string', description: 'Dev server URL (default: .weft/dev.json, then environments.dev.url)' }, tab: { type: 'string', description: 'Tab id from browser_tabs (default: the focused or latest tab)' } }
|
|
33
|
-
const t = (name, description, properties = {}, required) => ({ name, description, inputSchema: { type: 'object', properties: { ...properties, ...tab }, ...(required ? { required } : {}) } })
|
|
34
|
-
return [
|
|
35
|
-
t('browser_tabs', 'Tabs of the app connected to the dev server. Any browser works (yours, the person\'s); with none, call browser_launch. If your own browser can run JS, `await __weft__.agent.run(command, args)` in the page does the same as these tools.'),
|
|
36
|
-
t('browser_launch', 'Start a headless Chrome on the app (only when you have no browser of your own)', { route: { type: 'string' }, headed: { type: 'boolean' } }),
|
|
37
|
-
t('browser_open', 'Navigate the tab to a route', { route: { type: 'string' } }, ['route']),
|
|
38
|
-
t('browser_snapshot', "The page as roles, names and refs (`- button \"Add Banana\" [ref=e15]`), plus weft's live reactive graph with real names (`Cart.total = 42.5 ← Cart.subtotal → 1 binding in <Cart>`), and errors since the last call. Big payloads come back as a file path.", {
|
|
39
|
-
interactive: { type: 'boolean', description: 'Only interactive elements' }, delta: { type: 'boolean', description: 'Only what changed since the last snapshot' },
|
|
40
|
-
scope: target, graph: { description: "false: no graph; 'only': only the graph; a string filters it by name", anyOf: [{ type: 'boolean' }, { type: 'string' }] },
|
|
41
|
-
}),
|
|
42
|
-
t('browser_act', `Act like a user (user-event, as in @weft-js/core/test) and get back what changed on the page. Actions: ${browserActions.join(', ')}`, { target, action: { type: 'string', enum: browserActions }, value: { type: 'string', description: 'fill/type text, press key (`{Enter}`), select option' } }, ['target', 'action']),
|
|
43
|
-
t('browser_assert', `Check what the user sees (retries up to timeout ms). Expectations: ${expectations.join(', ')}. Recorded for browser_record.`, { target, expect: { type: 'object', description: '{ text: "Total $4" } | { value: "SAVE10" } | { disabled: true } | { count: 3 } | { absent: true } | …' }, timeout: { type: 'number' } }, ['target', 'expect']),
|
|
44
|
-
t('browser_state', "Read the named reactive graph, or write a box by its graph name to reach a state fast (dev only)", { filter: { type: 'string', description: 'Name prefix (`Cart`, `cart.`)' }, set: { type: 'string', description: 'Graph name to write, e.g. Cart.coupon' }, value: { description: 'JSON value to write' } }),
|
|
45
|
-
t('browser_time', "The page's clock: pause (reload: from page load, so startup timers are faked too), advance ms (runs due timers in order), resume, status", { op: { type: 'string', enum: ['pause', 'advance', 'resume', 'status'] }, ms: { type: 'number' }, reload: { type: 'boolean' } }, ['op']),
|
|
46
|
-
t('browser_net', 'The network as the app sees it: offline (navigator.onLine false, offline event, fetch fails, sockets dropped), online, latency ms (fetch), drop (close open WebSockets/EventSources: a server restart), reset, status', { op: { type: 'string', enum: ['offline', 'online', 'latency', 'drop', 'reset', 'status'] }, ms: { type: 'number' } }, ['op']),
|
|
47
|
-
t('browser_record', 'Record what happens in the tab (your acts and asserts, and clicks/typing from any browser tool) and turn it into a @weft-js/core/test test, which then has to pass the gate', {
|
|
48
|
-
op: { type: 'string', enum: ['start', 'stop', 'steps', 'note', 'code'] }, text: { type: 'string' }, component: { type: 'string' }, from: { type: 'string' }, props: { type: 'string' }, name: { type: 'string' }, out: { type: 'string', description: 'Write the test to this file (relative to the project)' },
|
|
49
|
-
}, ['op']),
|
|
50
|
-
t('browser_logs', 'Console errors and warnings, and uncaught errors with the weft binding they came from, since the last call'),
|
|
51
|
-
t('browser_screenshot', 'PNG of the launched Chrome (browser_launch); in your own browser, use its screenshot tool', { out: { type: 'string' } }),
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
21
|
/** What `weft mcp` offers an agent: each tool's name, its description and the arguments it takes. */
|
|
56
22
|
export const tools = [
|
|
57
23
|
{
|
|
58
24
|
name: 'docs',
|
|
59
25
|
description: `weft docs for agents. Topics: ${Object.keys(topics).join(', ')}; or a diagnostic code such as W014. No topic: the index.`,
|
|
60
|
-
inputSchema: {
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: { topic: { type: 'string', description: 'Topic name or diagnostic code' } },
|
|
29
|
+
},
|
|
61
30
|
},
|
|
62
31
|
{
|
|
63
32
|
name: 'check',
|
|
64
|
-
description:
|
|
65
|
-
|
|
33
|
+
description:
|
|
34
|
+
'Run `weft check`: every W- and P-diagnostic with code, file, span, why and fix (edits when an autofix exists), plus slot bindings.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
files: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'string' },
|
|
41
|
+
description: 'Files relative to the project root; default: all',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
66
45
|
},
|
|
67
46
|
{
|
|
68
47
|
name: 'routes_meta',
|
|
69
|
-
description:
|
|
70
|
-
|
|
48
|
+
description:
|
|
49
|
+
"Every route's metadata as a crawler gets it (`weft routes --meta --json`): title, description, image, every tag, where each came from (`route`, `layout`, `config`, `auto`, `card`), real samples of param routes, and H-diagnostics with text to apply. Every page sets `meta` from its own context as part of building it: call this after adding or changing a route.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
preset: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: "`static`: no server at runtime, so a param route's card needs `prerender`",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
71
59
|
},
|
|
72
60
|
{
|
|
73
61
|
name: 'audit',
|
|
74
|
-
description:
|
|
75
|
-
|
|
62
|
+
description:
|
|
63
|
+
"The pages as a browser renders them (`weft audit`): a clean Chrome at a phone's and a desktop's width, axe-core's WCAG rules (what Lighthouse grades), console errors, deprecations, the head a crawler reads, sideways scroll and layout shift, as L-diagnostics with fixes. Static checks read source; this catches what CSS and a breakpoint do to it. Default: the last build's pages.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
url: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'A running site to audit instead (a deploy, `weft start`, the dev server)',
|
|
70
|
+
},
|
|
71
|
+
indexing: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
enum: ['auto', 'public', 'private'],
|
|
74
|
+
description:
|
|
75
|
+
'Public makes noindex in markup or HTTP headers an error. Private preserves intentional noindex. Default inherits build.audit.indexing.',
|
|
76
|
+
},
|
|
77
|
+
routes: {
|
|
78
|
+
type: 'array',
|
|
79
|
+
items: { type: 'string' },
|
|
80
|
+
description: 'Paths; default one page of each kind',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
76
84
|
},
|
|
77
85
|
{
|
|
78
86
|
name: 'map',
|
|
79
|
-
description:
|
|
87
|
+
description:
|
|
88
|
+
'What this app has and what to call for each part: routes, server functions, data, styles, plugins',
|
|
80
89
|
inputSchema: { type: 'object', properties: {} },
|
|
81
90
|
},
|
|
82
91
|
{
|
|
83
92
|
name: 'tasks',
|
|
84
|
-
description:
|
|
93
|
+
description:
|
|
94
|
+
"List the project's commands and tasks: what each does, its args schema, side effects and allowed environments.",
|
|
85
95
|
inputSchema: { type: 'object', properties: {} },
|
|
86
96
|
},
|
|
87
97
|
{
|
|
88
98
|
name: 'style_find',
|
|
89
|
-
description:
|
|
90
|
-
|
|
99
|
+
description:
|
|
100
|
+
"Before styling anything, find the style groups that already fit (`weft style find`): name, doc, axes and values, parts, states, how often it's used, and similar groups. Reuse or add an axis value; don't write a new group or class list for the same thing.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
query: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description:
|
|
107
|
+
'What the thing is: `card`, `status badge`, `nav`, or a token such as `critical`',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['query'],
|
|
111
|
+
},
|
|
91
112
|
},
|
|
92
113
|
{
|
|
93
114
|
name: 'style_explain',
|
|
94
|
-
description:
|
|
115
|
+
description:
|
|
116
|
+
'Explain one variation of a style group (`weft style explain`): every declaration on its root and parts in cascade order with the token, axis, state, compound or accent it came from, the attributes it renders, the CSS it emits, and the call sites that produce it.',
|
|
95
117
|
inputSchema: {
|
|
96
118
|
type: 'object',
|
|
97
119
|
properties: {
|
|
98
|
-
target: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
target: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description:
|
|
123
|
+
'A group (`order-card`), a part (`order-card.title`), `order-card[status=late][selected]`, or a DOM selector (`[data-g="order-card"][data-status="late"]`)',
|
|
124
|
+
},
|
|
125
|
+
axes: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
additionalProperties: { type: 'string' },
|
|
128
|
+
description: 'Axis values, e.g. { "status": "late" } (defaults fill the rest)',
|
|
129
|
+
},
|
|
130
|
+
states: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: { type: 'string' },
|
|
133
|
+
description: 'States to apply: selected, hover…',
|
|
134
|
+
},
|
|
135
|
+
accent: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
additionalProperties: { type: 'string' },
|
|
138
|
+
description: 'A call-site accent, intent → token: { "radius": "round" }',
|
|
139
|
+
},
|
|
102
140
|
part: { type: 'string', description: 'Only this part (and the root)' },
|
|
103
141
|
},
|
|
104
142
|
required: ['target'],
|
|
@@ -106,13 +144,26 @@ export const tools = [
|
|
|
106
144
|
},
|
|
107
145
|
{
|
|
108
146
|
name: 'surfaces',
|
|
109
|
-
description:
|
|
147
|
+
description:
|
|
148
|
+
'List the project\'s canvas and WebGL surfaces (`weft surfaces`): each `defineSurface` with its name, kind, props and file, ready to drop in as `<Surface use={X} label="…" />`.',
|
|
110
149
|
inputSchema: { type: 'object', properties: {} },
|
|
111
150
|
},
|
|
112
151
|
{
|
|
113
152
|
name: 'test_gate',
|
|
114
|
-
description:
|
|
115
|
-
|
|
153
|
+
description:
|
|
154
|
+
'Run the test gate on the diff (`weft test --gate`): each new or changed test must fail on the old code (T010), kill a mutant of the changed lines (T011), and no existing test may get weaker (T020). Returns per-test results, surviving mutants with what to assert, and diagnostics.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
base: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'Git ref to diff against (default HEAD: the uncommitted change)',
|
|
161
|
+
},
|
|
162
|
+
budgetSec: { type: 'number' },
|
|
163
|
+
maxMutants: { type: 'number' },
|
|
164
|
+
refactor: { type: 'boolean' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
116
167
|
},
|
|
117
168
|
...browserTools(),
|
|
118
169
|
]
|
|
@@ -120,15 +171,19 @@ export const tools = [
|
|
|
120
171
|
async function callBrowser(root, name, args) {
|
|
121
172
|
const o = { url: args.url, tab: args.tab }
|
|
122
173
|
const cmd = name.slice('browser_'.length)
|
|
123
|
-
if (cmd === 'state')
|
|
174
|
+
if (cmd === 'state')
|
|
175
|
+
return args.set
|
|
176
|
+
? browser(root, 'set', { name: args.set, value: args.value }, o)
|
|
177
|
+
: browser(root, 'graph', { filter: args.filter }, o)
|
|
124
178
|
return browser(root, cmd, args, o)
|
|
125
179
|
}
|
|
126
180
|
|
|
127
181
|
/** Handle one JSON-RPC message. Returns the response, or undefined for notifications. */
|
|
128
182
|
export async function handle(root, msg) {
|
|
129
|
-
const reply = result => ({ jsonrpc: '2.0', id: msg.id, result })
|
|
183
|
+
const reply = (result) => ({ jsonrpc: '2.0', id: msg.id, result })
|
|
130
184
|
const fail = (code, message) => ({ jsonrpc: '2.0', id: msg.id ?? null, error: { code, message } })
|
|
131
|
-
if (msg?.jsonrpc !== '2.0' || typeof msg.method !== 'string')
|
|
185
|
+
if (msg?.jsonrpc !== '2.0' || typeof msg.method !== 'string')
|
|
186
|
+
return fail(-32600, 'Invalid Request')
|
|
132
187
|
const notification = msg.id === undefined
|
|
133
188
|
switch (msg.method) {
|
|
134
189
|
case 'initialize': {
|
|
@@ -136,8 +191,9 @@ export async function handle(root, msg) {
|
|
|
136
191
|
return reply({
|
|
137
192
|
protocolVersion: versions.includes(asked) ? asked : versions[0],
|
|
138
193
|
capabilities: { tools: { listChanged: false } },
|
|
139
|
-
serverInfo: { name: 'weft', version: '0.1.
|
|
140
|
-
instructions:
|
|
194
|
+
serverInfo: { name: 'weft', version: '0.1.2' },
|
|
195
|
+
instructions:
|
|
196
|
+
'weft is a fine-grained reactive TSX framework, not React. Read `docs` topic `rules` first, and run `check` after every edit. Every page sets `meta` from its own context as part of building it (`docs` topic `meta`). Chrome shared across route sections (a sidebar) goes in one layout above them that follows `currentPath()`, never a layout per section (`docs` topic `start`). Style with groups from @weft-js/core/style, not class lists: `style_find` before you write one. Verify behavior in the running app with the browser_* tools (or `__weft__.agent` in your own browser), and run `test_gate` before you call a change done.',
|
|
141
197
|
})
|
|
142
198
|
}
|
|
143
199
|
case 'ping':
|
|
@@ -154,18 +210,40 @@ export async function handle(root, msg) {
|
|
|
154
210
|
}
|
|
155
211
|
if (name === 'check') data = await check(root, { files: args.files })
|
|
156
212
|
else if (name === 'tasks') data = await tasks(root)
|
|
157
|
-
else if (name === 'routes_meta')
|
|
158
|
-
|
|
213
|
+
else if (name === 'routes_meta')
|
|
214
|
+
data = await routesCommand(root, { meta: true, preset: args.preset })
|
|
215
|
+
else if (name === 'audit')
|
|
216
|
+
data = await audit(root, { url: args.url, routes: args.routes, indexing: args.indexing })
|
|
159
217
|
else if (name === 'map') data = await agentMap(root)
|
|
160
|
-
else if (name === 'style_find')
|
|
161
|
-
|
|
218
|
+
else if (name === 'style_find')
|
|
219
|
+
data = findGroups((await styleMatrix(root)).matrix, args.query)
|
|
220
|
+
else if (name === 'style_explain')
|
|
221
|
+
data = explainTarget(await styleMatrix(root), args.target, {
|
|
222
|
+
axes: args.axes,
|
|
223
|
+
states: args.states,
|
|
224
|
+
accent: args.accent,
|
|
225
|
+
part: args.part,
|
|
226
|
+
})
|
|
162
227
|
else if (name === 'surfaces') data = { surfaces: listSurfaces(root) }
|
|
163
|
-
else if (name === 'test_gate')
|
|
228
|
+
else if (name === 'test_gate')
|
|
229
|
+
data = await gate(root, {
|
|
230
|
+
base: args.base,
|
|
231
|
+
budgetSec: args.budgetSec,
|
|
232
|
+
maxMutants: args.maxMutants,
|
|
233
|
+
refactor: args.refactor,
|
|
234
|
+
})
|
|
164
235
|
else if (name.startsWith('browser_')) data = await callBrowser(root, name, args)
|
|
165
236
|
else return fail(-32602, `Unknown tool: ${name}`)
|
|
166
|
-
return reply({
|
|
237
|
+
return reply({
|
|
238
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
239
|
+
structuredContent: data,
|
|
240
|
+
isError: false,
|
|
241
|
+
})
|
|
167
242
|
} catch (error) {
|
|
168
|
-
return reply({
|
|
243
|
+
return reply({
|
|
244
|
+
content: [{ type: 'text', text: `${error.code ?? 'E_INTERNAL'}: ${error.message}` }],
|
|
245
|
+
isError: true,
|
|
246
|
+
})
|
|
169
247
|
}
|
|
170
248
|
}
|
|
171
249
|
default:
|
|
@@ -176,9 +254,9 @@ export async function handle(root, msg) {
|
|
|
176
254
|
/** Answer newline-delimited JSON-RPC from `input`, one message at a time; resolves once `input` has closed and the last answer is out. */
|
|
177
255
|
export function serve(root, input = process.stdin, output = process.stdout) {
|
|
178
256
|
const rl = createInterface({ input, crlfDelay: Infinity })
|
|
179
|
-
const write = obj => output.write(`${JSON.stringify(obj)}\n`)
|
|
257
|
+
const write = (obj) => output.write(`${JSON.stringify(obj)}\n`)
|
|
180
258
|
let queue = Promise.resolve()
|
|
181
|
-
rl.on('line', line => {
|
|
259
|
+
rl.on('line', (line) => {
|
|
182
260
|
if (!line.trim()) return
|
|
183
261
|
queue = queue.then(async () => {
|
|
184
262
|
let msg
|
package/src/meta-table.mjs
CHANGED
|
@@ -9,20 +9,55 @@ const short = (s, n) => {
|
|
|
9
9
|
const cell = (s, n) => short(s, n).padEnd(n)
|
|
10
10
|
|
|
11
11
|
/** `/_weft/og/users/42.png?v=abc` → `card: users/42.png`; an explicit image as it is. */
|
|
12
|
-
const imageOf =
|
|
12
|
+
const imageOf = (url) =>
|
|
13
|
+
!url
|
|
14
|
+
? 'none'
|
|
15
|
+
: url
|
|
16
|
+
.replace(/^https?:\/\/[^/]+/, '')
|
|
17
|
+
.replace(/^\/_weft\/og\//, '/_weft/og/')
|
|
18
|
+
.replace(/\?v=.*/, '')
|
|
13
19
|
|
|
14
20
|
export function metaTable(result) {
|
|
15
21
|
const rows = []
|
|
16
22
|
for (const r of result.routes) {
|
|
17
23
|
if (r.kind !== 'page' || r.redirect !== undefined) continue
|
|
18
|
-
const add = (path, title, description, image, source) =>
|
|
24
|
+
const add = (path, title, description, image, source) =>
|
|
25
|
+
rows.push({
|
|
26
|
+
path,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
image: imageOf(image),
|
|
30
|
+
source: source ? `${source.title}/${source.description}/${source.image}` : 'source only',
|
|
31
|
+
})
|
|
19
32
|
if (r.samples?.length) {
|
|
20
|
-
r.samples.slice(0, 3).forEach(s => add(s.url, s.title, s.description, s.image, s.source))
|
|
21
|
-
if (r.samples.length > 3)
|
|
22
|
-
|
|
33
|
+
r.samples.slice(0, 3).forEach((s) => add(s.url, s.title, s.description, s.image, s.source))
|
|
34
|
+
if (r.samples.length > 3)
|
|
35
|
+
rows.push({
|
|
36
|
+
path: ` … ${r.samples.length - 3} more of ${r.path}`,
|
|
37
|
+
title: '',
|
|
38
|
+
description: '',
|
|
39
|
+
image: '',
|
|
40
|
+
source: '',
|
|
41
|
+
})
|
|
42
|
+
} else
|
|
43
|
+
add(r.url ?? r.path, r.meta?.title, r.meta?.description, r.meta?.og?.image?.url, r.source)
|
|
23
44
|
}
|
|
24
|
-
const width = {
|
|
25
|
-
|
|
45
|
+
const width = {
|
|
46
|
+
path: Math.min(30, Math.max(5, ...rows.map((r) => r.path.length))),
|
|
47
|
+
image: Math.min(34, Math.max(5, ...rows.map((r) => r.image.length))),
|
|
48
|
+
}
|
|
49
|
+
const line = (r) =>
|
|
50
|
+
`${cell(r.path, width.path)} ${cell(r.title, 36)} ${cell(r.description, 48)} ${cell(r.image, width.image)} ${r.source}`
|
|
26
51
|
const url = result.site?.url ? `site.url ${result.site.url}` : 'site.url not set'
|
|
27
|
-
return [
|
|
52
|
+
return [
|
|
53
|
+
`${url}. source: title/description/image, each route, layout, config, auto or card.`,
|
|
54
|
+
line({
|
|
55
|
+
path: 'route',
|
|
56
|
+
title: 'title',
|
|
57
|
+
description: 'description',
|
|
58
|
+
image: 'image',
|
|
59
|
+
source: 'source',
|
|
60
|
+
}),
|
|
61
|
+
...rows.map(line),
|
|
62
|
+
].join('\n')
|
|
28
63
|
}
|