@wix/agent-skills 1.18.2 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +42 -40
- package/skills/wix-app/references/BACKEND_API.md +151 -152
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +12 -8
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
- package/skills/wix-app/scripts/pkg-root.cjs +73 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
- package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
- package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
- package/skills/wix-base44-connector/SKILL.md +24 -22
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless-fast/SKILL.md +25 -7
- package/skills/wix-headless-fast/entry/skill.md +23 -14
- package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
- package/skills/wix-manage/SKILL.md +14 -8
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +59 -33
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +45 -22
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +70 -12
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +53 -17
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +17 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +11 -3
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +5 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +5 -4
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +33 -5
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// oxlint-disable no-console preserve-caught-error
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Accessibility review for Editor React Components: one command, one JSON report.
|
|
8
|
+
*
|
|
9
|
+
* node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>
|
|
10
|
+
*
|
|
11
|
+
* Runs the jsx-a11y ESLint scanner and the Babel semantic/contract scanner in
|
|
12
|
+
* process, spawns the render audit (SSR + jsdom + axe-core) per component
|
|
13
|
+
* folder, and groups the findings by rule.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes: 0 every scanner ran and found nothing, 1 findings, 2 a scanner
|
|
16
|
+
* could not run or could not load the component (never treat 2 as clean).
|
|
17
|
+
* Test hook: `A11Y_SKIP_RENDER=1` skips the render audit; the review is then
|
|
18
|
+
* inconclusive (exit 2) even with no findings.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const ROOT = process.cwd();
|
|
26
|
+
const RENDER_SCRIPT = path.join(__dirname, 'scan-a11y-render.cjs');
|
|
27
|
+
const RENDER_TIMEOUT_MS = Number(process.env.A11Y_RENDER_TIMEOUT_MS) || 20_000;
|
|
28
|
+
const MAX_LOCATIONS_PER_GROUP = 5;
|
|
29
|
+
const SEVERITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
30
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2, unknown: 3 };
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Arguments and discovery
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const options = { targets: [], render: process.env.A11Y_SKIP_RENDER !== '1' };
|
|
38
|
+
for (const arg of argv) {
|
|
39
|
+
if (arg.startsWith('--')) throw new Error(`Unknown option ${arg}`);
|
|
40
|
+
options.targets.push(arg);
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isSource = (name) =>
|
|
46
|
+
/\.(tsx|jsx)$/.test(name) && !/\.(stories|test|spec)\.[jt]sx$/.test(name);
|
|
47
|
+
|
|
48
|
+
function listSources(dir, acc = []) {
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const full = path.join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (entry.name !== 'node_modules') listSources(full, acc);
|
|
53
|
+
} else if (entry.isFile() && isSource(entry.name)) acc.push(full);
|
|
54
|
+
}
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isComponentDir(dir) {
|
|
59
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
60
|
+
const names = fs.readdirSync(dir);
|
|
61
|
+
return names.includes('component.tsx') || names.some((name) => name.endsWith('.props.ts'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findComponentDir(file) {
|
|
65
|
+
let dir = path.dirname(file);
|
|
66
|
+
for (let depth = 0; depth < 6 && !isComponentDir(dir); depth++) {
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir) return path.dirname(file);
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
return isComponentDir(dir) ? dir : path.dirname(file);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Expand targets (component folders, a components root, or files) into files and component folders. */
|
|
75
|
+
function discoverFiles(targets) {
|
|
76
|
+
const files = new Set();
|
|
77
|
+
const componentDirs = new Set();
|
|
78
|
+
for (const target of targets) {
|
|
79
|
+
const resolved = path.resolve(target);
|
|
80
|
+
if (!fs.existsSync(resolved)) throw new Error(`Path not found: ${target}`);
|
|
81
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
82
|
+
if (isComponentDir(resolved)) componentDirs.add(resolved);
|
|
83
|
+
else {
|
|
84
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
85
|
+
const child = path.join(resolved, entry.name);
|
|
86
|
+
if (entry.isDirectory() && isComponentDir(child)) componentDirs.add(child);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const file of listSources(resolved)) files.add(file);
|
|
90
|
+
} else {
|
|
91
|
+
files.add(resolved);
|
|
92
|
+
componentDirs.add(findComponentDir(resolved));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { files: [...files].toSorted(), componentDirs: [...componentDirs].toSorted() };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Scanners
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function runScanner(name, load) {
|
|
103
|
+
const started = Date.now();
|
|
104
|
+
try {
|
|
105
|
+
const value = await load();
|
|
106
|
+
const parseErrors = (value.meta && value.meta.parseErrors) || [];
|
|
107
|
+
if (parseErrors.length) {
|
|
108
|
+
throw new Error(parseErrors.map((e) => `${e.file}: ${e.message}`).join('; '));
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, ms: Date.now() - started, value };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error && error.message ? error.message : String(error);
|
|
113
|
+
return { ok: false, ms: Date.now() - started, error: `${name}: ${message}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eslintFindings(report) {
|
|
118
|
+
return report.findings.map((f) => ({
|
|
119
|
+
rule: f.rule,
|
|
120
|
+
source: 'eslint',
|
|
121
|
+
severity: 'high',
|
|
122
|
+
confidence: 'high',
|
|
123
|
+
message: f.message,
|
|
124
|
+
at: `${f.file}:${f.line}:${f.column}`,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function semanticFindings(report) {
|
|
129
|
+
return report.findings.map((f) => {
|
|
130
|
+
const contract = f.sourceKind === 'contract';
|
|
131
|
+
const severity =
|
|
132
|
+
contract || f.confidence === 'high' ? 'high' : f.confidence === 'medium' ? 'medium' : 'low';
|
|
133
|
+
return {
|
|
134
|
+
rule: f.rule,
|
|
135
|
+
source: contract ? 'contract' : 'semantic',
|
|
136
|
+
severity,
|
|
137
|
+
confidence: f.confidence,
|
|
138
|
+
message: f.message,
|
|
139
|
+
detail: contract ? null : f.evidence,
|
|
140
|
+
at: `${f.file}:${f.line}:${f.column}`,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runRenderChild(componentDir) {
|
|
146
|
+
const relativeDir = path.relative(ROOT, componentDir) || '.';
|
|
147
|
+
const started = Date.now();
|
|
148
|
+
const result = spawnSync(process.execPath, [RENDER_SCRIPT, componentDir], {
|
|
149
|
+
cwd: ROOT,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
timeout: RENDER_TIMEOUT_MS,
|
|
152
|
+
killSignal: 'SIGKILL',
|
|
153
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
154
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' },
|
|
155
|
+
});
|
|
156
|
+
const base = { componentDir: relativeDir, ms: Date.now() - started };
|
|
157
|
+
|
|
158
|
+
if (result.error && result.error.code === 'ETIMEDOUT') {
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: 'timeout',
|
|
163
|
+
findings: [
|
|
164
|
+
{
|
|
165
|
+
rule: 'render-timeout',
|
|
166
|
+
source: 'render',
|
|
167
|
+
severity: 'high',
|
|
168
|
+
confidence: 'high',
|
|
169
|
+
message: `Rendering ${relativeDir} did not finish within ${RENDER_TIMEOUT_MS} ms. Rerun once; if it repeats, the first render never settles.`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let report = null;
|
|
176
|
+
try {
|
|
177
|
+
const lastLine = String(result.stdout || '')
|
|
178
|
+
.trim()
|
|
179
|
+
.split('\n')
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.pop();
|
|
182
|
+
report = lastLine ? JSON.parse(lastLine) : null;
|
|
183
|
+
} catch {
|
|
184
|
+
report = null;
|
|
185
|
+
}
|
|
186
|
+
if (!report) {
|
|
187
|
+
const error = truncate(result.stderr || `exit ${result.status}`, 400);
|
|
188
|
+
return { ...base, ok: false, reason: 'crash', error, findings: [] };
|
|
189
|
+
}
|
|
190
|
+
if (!report.ok) {
|
|
191
|
+
return {
|
|
192
|
+
...base,
|
|
193
|
+
ok: false,
|
|
194
|
+
reason: report.reason || 'error',
|
|
195
|
+
error: report.error || null,
|
|
196
|
+
findings: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
ok: true,
|
|
202
|
+
entry: report.entry,
|
|
203
|
+
preview: report.preview,
|
|
204
|
+
ssr: report.ssr,
|
|
205
|
+
axeRules: report.axe ? report.axe.rulesRun : null,
|
|
206
|
+
notChecked: report.notChecked || [],
|
|
207
|
+
findings: report.findings.map((f) => ({
|
|
208
|
+
...f,
|
|
209
|
+
source: 'render',
|
|
210
|
+
at: f.target ? `${relativeDir} ${f.target}` : relativeDir,
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// Report
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
function truncate(text, length) {
|
|
220
|
+
const compact = String(text || '').replace(/\s+/g, ' ');
|
|
221
|
+
return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** One entry per (source, rule, message); repeated nodes become a count plus a few locations. */
|
|
225
|
+
function groupFindings(findings, { capPerGroup = MAX_LOCATIONS_PER_GROUP } = {}) {
|
|
226
|
+
const groups = new Map();
|
|
227
|
+
for (const f of findings) {
|
|
228
|
+
const severity = f.severity in SEVERITY_RANK ? f.severity : 'medium';
|
|
229
|
+
const confidence = f.confidence in CONFIDENCE_RANK ? f.confidence : 'medium';
|
|
230
|
+
const key = `${f.source}|${f.rule}|${f.message}`;
|
|
231
|
+
if (!groups.has(key)) {
|
|
232
|
+
groups.set(key, {
|
|
233
|
+
rule: f.rule,
|
|
234
|
+
src: f.source,
|
|
235
|
+
sev: severity,
|
|
236
|
+
conf: confidence,
|
|
237
|
+
n: 0,
|
|
238
|
+
at: [],
|
|
239
|
+
html: f.html || null,
|
|
240
|
+
msg: f.message,
|
|
241
|
+
detail: f.detail || null,
|
|
242
|
+
helpUrl: f.helpUrl || null,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const group = groups.get(key);
|
|
246
|
+
group.n++;
|
|
247
|
+
if (f.at && group.at.length < capPerGroup && !group.at.includes(f.at)) group.at.push(f.at);
|
|
248
|
+
if (SEVERITY_RANK[severity] < SEVERITY_RANK[group.sev]) group.sev = severity;
|
|
249
|
+
if (CONFIDENCE_RANK[confidence] < CONFIDENCE_RANK[group.conf]) group.conf = confidence;
|
|
250
|
+
}
|
|
251
|
+
return [...groups.values()].toSorted(
|
|
252
|
+
(a, b) =>
|
|
253
|
+
SEVERITY_RANK[a.sev] - SEVERITY_RANK[b.sev] ||
|
|
254
|
+
CONFIDENCE_RANK[a.conf] - CONFIDENCE_RANK[b.conf] ||
|
|
255
|
+
b.n - a.n ||
|
|
256
|
+
a.rule.localeCompare(b.rule),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderLabel(result) {
|
|
261
|
+
if (result.ok) {
|
|
262
|
+
if (!result.ssr || !result.ssr.ok) return `render: ssr FAILED (${result.entry || '?'})`;
|
|
263
|
+
return `render ok (${result.entry || '?'}, axe ${result.axeRules ?? '-'})`;
|
|
264
|
+
}
|
|
265
|
+
return `render FAILED (${result.reason})`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildSummary(groups, scanners, { renderEnabled = true } = {}) {
|
|
269
|
+
const counts = { high: 0, medium: 0, low: 0, groups: groups.length, findings: 0 };
|
|
270
|
+
for (const group of groups) {
|
|
271
|
+
counts[group.sev] += group.n;
|
|
272
|
+
counts.findings += group.n;
|
|
273
|
+
}
|
|
274
|
+
// A timeout is itself a finding; any other render failure (missing tooling,
|
|
275
|
+
// a module the audit cannot load) or a skipped render means the audit did
|
|
276
|
+
// not run, never clean.
|
|
277
|
+
const staticFailed = !scanners.eslint.ok || !scanners.semantic.ok;
|
|
278
|
+
const renderBroken =
|
|
279
|
+
!renderEnabled || scanners.render.some((r) => !r.ok && r.reason !== 'timeout');
|
|
280
|
+
const status =
|
|
281
|
+
staticFailed || renderBroken ? 'error' : counts.findings > 0 ? 'findings' : 'clean';
|
|
282
|
+
const severities = ['high', 'medium', 'low']
|
|
283
|
+
.filter((level) => counts[level] > 0)
|
|
284
|
+
.map((level) => `${counts[level]} ${level}`);
|
|
285
|
+
const line = [
|
|
286
|
+
`A11Y ${counts.groups} groups/${counts.findings} findings${severities.length ? ` (${severities.join(', ')})` : ''}`,
|
|
287
|
+
`eslint ${scanners.eslint.ok ? 'ok' : 'FAILED'}`,
|
|
288
|
+
`semantic ${scanners.semantic.ok ? 'ok' : 'FAILED'}`,
|
|
289
|
+
renderEnabled
|
|
290
|
+
? scanners.render.map(renderLabel).join(' · ') || 'render: no component folder'
|
|
291
|
+
: 'render SKIPPED (inconclusive)',
|
|
292
|
+
].join(' · ');
|
|
293
|
+
return { status, line, counts };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const computeExitCode = (summary) =>
|
|
297
|
+
summary.status === 'error' ? 2 : summary.counts.findings > 0 ? 1 : 0;
|
|
298
|
+
|
|
299
|
+
async function review(options) {
|
|
300
|
+
const { files, componentDirs } = discoverFiles(options.targets);
|
|
301
|
+
// A wrong path (a typo, a parent folder) must never read as clean.
|
|
302
|
+
if (componentDirs.length === 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`No component folder under ${options.targets.join(', ')}: expected component.tsx or *.props.ts.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ESLint rejects an empty file list; a folder with no JSX still gets its render audit.
|
|
309
|
+
const eslint = await runScanner('eslint', () =>
|
|
310
|
+
files.length ? require('./scan-a11y-eslint.cjs').scan(files) : { findings: [] },
|
|
311
|
+
);
|
|
312
|
+
const semantic = await runScanner('semantic', () =>
|
|
313
|
+
files.length ? require('./scan-a11y-code.cjs').scan(files) : { findings: [] },
|
|
314
|
+
);
|
|
315
|
+
const render = options.render ? componentDirs.map(runRenderChild) : [];
|
|
316
|
+
|
|
317
|
+
const findings = [
|
|
318
|
+
...(eslint.ok ? eslintFindings(eslint.value) : []),
|
|
319
|
+
...(semantic.ok ? semanticFindings(semantic.value) : []),
|
|
320
|
+
...render.flatMap((r) => r.findings),
|
|
321
|
+
];
|
|
322
|
+
const groups = groupFindings(findings);
|
|
323
|
+
|
|
324
|
+
const notChecked = new Set(options.render ? [] : ['render audit (skipped)']);
|
|
325
|
+
for (const r of render) {
|
|
326
|
+
for (const item of r.notChecked || []) notChecked.add(item);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const scanners = {
|
|
330
|
+
eslint: { ok: eslint.ok, ms: eslint.ms, error: eslint.error || null },
|
|
331
|
+
semantic: { ok: semantic.ok, ms: semantic.ms, error: semantic.error || null },
|
|
332
|
+
render: render.map(({ findings: _findings, notChecked: _notChecked, ...rest }) => rest),
|
|
333
|
+
};
|
|
334
|
+
const summary = buildSummary(groups, scanners, { renderEnabled: options.render });
|
|
335
|
+
return {
|
|
336
|
+
summary,
|
|
337
|
+
findings: groups,
|
|
338
|
+
scanners,
|
|
339
|
+
files: files.map((file) => path.relative(ROOT, file)),
|
|
340
|
+
notChecked: [...notChecked],
|
|
341
|
+
exitCode: computeExitCode(summary),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function main() {
|
|
346
|
+
let options;
|
|
347
|
+
try {
|
|
348
|
+
options = parseArgs(process.argv.slice(2));
|
|
349
|
+
if (options.targets.length === 0) throw new Error('No component directory or files specified.');
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(
|
|
352
|
+
`${error.message}\nUsage: node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>`,
|
|
353
|
+
);
|
|
354
|
+
process.exit(2);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const report = await review(options);
|
|
358
|
+
console.log(JSON.stringify(report));
|
|
359
|
+
process.exit(report.exitCode);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.log(
|
|
362
|
+
JSON.stringify({
|
|
363
|
+
summary: { status: 'error', line: error.message, counts: { findings: 0 } },
|
|
364
|
+
findings: [],
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
process.exit(2);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = { review, discoverFiles, groupFindings, buildSummary, computeExitCode };
|
|
372
|
+
|
|
373
|
+
if (require.main === module) main();
|
|
@@ -47,6 +47,9 @@ management tools, or do both.
|
|
|
47
47
|
visitor, never the admin connector token. Call Wix directly from the browser through one shared
|
|
48
48
|
visitor client (Write the code, below). Redirect sessions for Wix-hosted flows also require a
|
|
49
49
|
visitor token minted for the headless OAuth app; see Visitor authentication and Wix-hosted flows below.
|
|
50
|
+
**The OAuth app is a one-call prerequisite, not a dead end**: `wx.ensureOAuthApp` returns its
|
|
51
|
+
`clientId`, creating the app when the site has none, so it is a visitor flow's first step — never
|
|
52
|
+
a reason to move the flow onto the admin token or leave it for later.
|
|
50
53
|
Anyone can mint an anonymous visitor token from the
|
|
51
54
|
OAuth app's public `clientId`; no visitor login is required. APIs for the "current visitor"
|
|
52
55
|
use that token to identify whose data and state to access. This applies both to a standalone
|
|
@@ -90,6 +93,7 @@ const wx = require(require("path").resolve(P));
|
|
|
90
93
|
- `wx.bash(cmd)` — shell over saved files (GNU grep/sed; awk is mawk; no rg)
|
|
91
94
|
- `wx.spec(docsUrl | code)` — a method's exact schema, plus the titles of the docs' own request examples saved at `examplesPath`; pass a hit's docsUrl (direct load), or raw code to query the index yourself
|
|
92
95
|
- `wx.mgmtRecipes(q?)` — management-recipe index; no arg → categories, a word → matching recipes
|
|
96
|
+
- `wx.ensureOAuthApp(token, { name, redirectUris, redirectDomains })` — the headless OAuth app, found by name or created, with the redirect lists merged into whatever it already allows; returns `{ clientId, created }`. `clientId` is what the browser mints visitor tokens from. Run it whenever `context` reports no OAuth app, or before any Wix-hosted flow that returns to this app — see Visitor authentication and Wix-hosted flows
|
|
93
97
|
- `wx.installApp(appDefId, siteId, token)` — install a Wix app on the site (Apps Installer). If discovery finds an API whose app isn't installed on the site, install it first — that's a one-call prerequisite, **not** a reason to fall back to a hand-built alternative. `appDefId` from `search` or the Apps-Created-by-Wix table; `siteId` from `context` (the site report)
|
|
94
98
|
|
|
95
99
|
Search makes one combined request across REST, management recipes, and Headless by default.
|
|
@@ -228,7 +232,9 @@ Want to shape the result yourself? Pass raw code against the index (`lightIndex`
|
|
|
228
232
|
await wx.spec(`
|
|
229
233
|
const url = "<docsUrl from search/browse>"; // API method page, not a skill/article page
|
|
230
234
|
const s = await getResourceSchemaByUrl(url);
|
|
231
|
-
|
|
235
|
+
// A method URL resolves to exactly that method — take methods[0]. Don't match on docsUrl:
|
|
236
|
+
// the reader normalizes the url, so equality against the one you passed can miss.
|
|
237
|
+
const m = s.methods[0];
|
|
232
238
|
return {
|
|
233
239
|
call: m.publicUrl, // callable https://www.wixapis.com/… URL
|
|
234
240
|
body: m.requestBody?.content["application/json"].schema.properties,
|
|
@@ -327,13 +333,15 @@ return await wx.post("<a public read from Learn Wix>", { query: {} }, visitorTok
|
|
|
327
333
|
|
|
328
334
|
**Any Wix-hosted flow that returns the visitor to your app needs the headless OAuth app's
|
|
329
335
|
redirect config set.** This covers redirect sessions *and* sending a buyer to the Wix-hosted
|
|
330
|
-
checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
visitors do not need to log in)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app.
|
|
337
|
+
|
|
338
|
+
**Step one of any visitor flow: `wx.ensureOAuthApp` with this app's own URLs.** It returns the
|
|
339
|
+
`clientId` the frontend mints visitor tokens from (anonymous visitors do not need to log in),
|
|
340
|
+
creating the OAuth app when the site has none and adding your URLs to its lists when it already
|
|
341
|
+
has one. Returns only complete for URLs in those lists, and a missing one breaks silently —
|
|
342
|
+
create-checkout and the anonymous token still succeed — until a real buyer is redirected and
|
|
343
|
+
stranded on Wix. It is idempotent by name: never create a second OAuth app for the same Base44
|
|
344
|
+
app, or the first `clientId`'s returns stop working.
|
|
337
345
|
|
|
338
346
|
```js
|
|
339
347
|
// Register BOTH of this app's own URLs — its preview URL and its published URL — so returns work
|
|
@@ -353,20 +361,14 @@ const returnDomains = appOrigins.map(origin => new URL(origin).hostname);
|
|
|
353
361
|
// OAuth redirect configuration: exact login URLs versus domains for other returns.
|
|
354
362
|
// https://dev.wix.com/docs/go-headless/authentication/setup/allow-redirect-uris-and-domains.md
|
|
355
363
|
const { accessToken: adminToken } = await base44.asServiceRole.connectors.getConnection("wix");
|
|
356
|
-
const {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}, adminToken);
|
|
365
|
-
const clientId = oAuthApp.id; // Public visitor client ID, used by the frontend client above.
|
|
366
|
-
|
|
367
|
-
// If destinations change later, update this OAuth app rather than creating another.
|
|
368
|
-
// Read its existing lists and merge new entries before updating, preserving old entries.
|
|
369
|
-
// https://dev.wix.com/docs/api-reference/business-management/headless/oauth-apps/update-oauth-app.md
|
|
364
|
+
const { clientId, created } = await wx.ensureOAuthApp(adminToken, {
|
|
365
|
+
name: "My App", // this app's name; the lookup key on later runs
|
|
366
|
+
redirectUris: loginCallbacks, // login callbacks: matched exactly by the auth request
|
|
367
|
+
redirectDomains: returnDomains, // returns from Wix-hosted flows: hostnames only
|
|
368
|
+
});
|
|
369
|
+
// clientId is the public visitor client id the frontend client above mints from.
|
|
370
|
+
// When destinations change later — a custom domain, a renamed app — call this again with the
|
|
371
|
+
// new URLs: it updates the same OAuth app and preserves the entries already there.
|
|
370
372
|
|
|
371
373
|
```
|
|
372
374
|
|
|
@@ -353,9 +353,13 @@ async function spec(arg) {
|
|
|
353
353
|
const code = /^async function/.test(s) ? s
|
|
354
354
|
: /^https:\/\/dev\.wix\.com\/docs\//.test(s) ? "async function(){ return await getResourceSchemaByUrl(" + JSON.stringify(s) + "); }"
|
|
355
355
|
: "async function(){ " + s + " }";
|
|
356
|
-
const { result } = await post("https://mcp.wix.com/api/code-mode/search", { code });
|
|
356
|
+
const { result, error } = await post("https://mcp.wix.com/api/code-mode/search", { code });
|
|
357
|
+
// The envelope is 200 even when the query throws — the cause is in `error`, not the status.
|
|
358
|
+
// Surfacing it matters: swallowing it turns "your code threw" into "nothing matched".
|
|
359
|
+
if (error)
|
|
360
|
+
return { error, note: "the query threw — read the message and fix the code; don't re-send it unchanged" };
|
|
357
361
|
if (result == null || (Array.isArray(result) && !result.length))
|
|
358
|
-
return { result, note: "
|
|
362
|
+
return { result, note: "the query ran but matched nothing — widen it, or check the url resolves" };
|
|
359
363
|
const text = JSON.stringify(result, null, 1);
|
|
360
364
|
if (text.length <= BUDGET) return result;
|
|
361
365
|
let h = 5381;
|
|
@@ -424,4 +428,36 @@ async function installApp(appDefId, siteId, token) {
|
|
|
424
428
|
{ tenant: { tenantType: "SITE", id: siteId }, appInstance: { appDefId } }, token);
|
|
425
429
|
}
|
|
426
430
|
|
|
427
|
-
|
|
431
|
+
// The headless OAuth app: the visitor client id, and the redirect config every Wix-hosted
|
|
432
|
+
// return needs. Idempotent by name — a site reached by the "connect an existing site" flow
|
|
433
|
+
// has none, and creating a second one for the same app strands the first app's returns.
|
|
434
|
+
// Redirect lists are merged, never replaced: an app that already serves a custom domain
|
|
435
|
+
// keeps it when a preview URL is added.
|
|
436
|
+
async function ensureOAuthApp(token, { name, redirectUris = [], redirectDomains = [] }) {
|
|
437
|
+
const { oAuthApps = [] } = await post(
|
|
438
|
+
"https://www.wixapis.com/oauth-app/v1/oauth-apps/query", { query: {} }, token);
|
|
439
|
+
const existing = oAuthApps.find((a) => a.name === name);
|
|
440
|
+
const merge = (a = [], b = []) => [...new Set([...a, ...b])];
|
|
441
|
+
if (!existing) {
|
|
442
|
+
const { oAuthApp } = await post("https://www.wixapis.com/oauth-app/v1/oauth-apps", {
|
|
443
|
+
oAuthApp: { name, allowedRedirectUris: redirectUris, allowedRedirectDomains: redirectDomains },
|
|
444
|
+
}, token);
|
|
445
|
+
return { clientId: oAuthApp.id, created: true, oAuthApp };
|
|
446
|
+
}
|
|
447
|
+
const allowedRedirectUris = merge(existing.allowedRedirectUris, redirectUris);
|
|
448
|
+
const allowedRedirectDomains = merge(existing.allowedRedirectDomains, redirectDomains);
|
|
449
|
+
const unchanged =
|
|
450
|
+
allowedRedirectUris.length === (existing.allowedRedirectUris || []).length &&
|
|
451
|
+
allowedRedirectDomains.length === (existing.allowedRedirectDomains || []).length;
|
|
452
|
+
if (unchanged) return { clientId: existing.id, created: false, oAuthApp: existing };
|
|
453
|
+
// The update docs disagree with themselves on the mask field (prose says `paths`, the
|
|
454
|
+
// curl example says `path`); send both so the call does not silently no-op.
|
|
455
|
+
const paths = ["allowedRedirectUris", "allowedRedirectDomains"];
|
|
456
|
+
const { oAuthApp } = await patch(
|
|
457
|
+
`https://www.wixapis.com/oauth-app/v1/oauth-apps/${existing.id}`,
|
|
458
|
+
{ oAuthApp: { id: existing.id, allowedRedirectUris, allowedRedirectDomains },
|
|
459
|
+
mask: { paths, path: paths.join(",") } }, token);
|
|
460
|
+
return { clientId: (oAuthApp || existing).id, created: false, oAuthApp: oAuthApp || existing };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { req, post, get, patch, put, del, clip, context, browse, search, page, bash, spec, mgmtRecipes, installApp, ensureOAuthApp };
|
package/skills/wix-docs/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wix-docs
|
|
3
|
-
description: "Look up the Wix API/SDK documentation to confirm an exact endpoint, HTTP method, request/response shape, field, enum, or error before writing Wix code — never guess a Wix API from memory. A lookup is a short flow: find the right page, then read it. Two ways: (1) plain `curl` (zero dependencies) — find a page by **semantic search** (`POST /mcp-docs-search/v1/docs/search`, natural-language `{ search_term, document_type }`, incl. the SKILLS recipe corpus for multi-step workflows) **or by browsing** a docs portal as a menu — a structured, typed, counted browse of the REST, SDK, CLI, Build Apps, and Headless portals (`POST /mcp-docs-search/v1/docs/menu/browse`), or the `.md` menu tree from the `llms.txt` root for any surface — then read the page by appending `.md` to its URL; (2) the Wix MCP doc tools when present. Triggers: look up a Wix API, find the Wix endpoint/method, confirm a Wix request body or field, verify a Wix API shape, explore Wix docs, which Wix API do I call, read a Wix method schema."
|
|
3
|
+
description: "Look up the Wix API/SDK documentation to confirm an exact endpoint, HTTP method, request/response shape, field, enum, or error before writing Wix code — never guess a Wix API from memory. A lookup is a short flow: find the right page, then read it. Two ways: (1) plain `curl` (zero dependencies) — find a page by **semantic search** (`POST /mcp-docs-search/v1/docs/search`, natural-language `{ search_term, document_type(s) }`, incl. the SKILLS recipe corpus for multi-step workflows) **or by browsing** a docs portal as a menu — a structured, typed, counted browse of the REST, SDK, CLI, Build Apps, and Headless portals (`POST /mcp-docs-search/v1/docs/menu/browse`), or the `.md` menu tree from the `llms.txt` root for any surface — then read the page by appending `.md` to its URL; (2) the Wix MCP doc tools when present. Triggers: look up a Wix API, find the Wix endpoint/method, confirm a Wix request body or field, verify a Wix API shape, explore Wix docs, which Wix API do I call, read a Wix method schema."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Wix Docs — look up the Wix API/SDK documentation
|
|
@@ -15,11 +15,11 @@ or the Wix MCP doc tools if your agent has them (Lane 2). Either way, route by w
|
|
|
15
15
|
know:
|
|
16
16
|
|
|
17
17
|
- **You have a docs URL** → just read it (§2). Don't re-search for a page you can already name.
|
|
18
|
-
- **A multi-step workflow** ("take a booking from service setup to payment") →
|
|
19
|
-
|
|
20
|
-
cross-step gotchas, and the one bundled endpoint that does
|
|
21
|
-
method page states. No relevant recipe
|
|
22
|
-
from verified per-method contracts.
|
|
18
|
+
- **A multi-step workflow** ("take a booking from service setup to payment") → include the **recipe**
|
|
19
|
+
corpus in the search: `document_types: ["SKILLS", …]` alongside the API corpus the steps live in
|
|
20
|
+
(§1A). A recipe carries step ordering, cross-step gotchas, and the one bundled endpoint that does
|
|
21
|
+
the whole job — things no single method page states. No relevant recipe comes back → assemble the
|
|
22
|
+
workflow from the verified per-method contracts in the same result set.
|
|
23
23
|
- **One specific operation, field, or enum** → search its API corpus (`REST` / `SDK`), then read or
|
|
24
24
|
schema-check what you land on.
|
|
25
25
|
|
|
@@ -34,12 +34,21 @@ Three ways to reach the right page — use whichever fits.
|
|
|
34
34
|
|
|
35
35
|
**A. Semantic search.** Describe what you want in natural language ("let a customer book an
|
|
36
36
|
appointment"), not just keywords; hits come back ranked by relevance. Same `POST` body for both
|
|
37
|
-
variants: `search_term` (required, 1–500), `document_type` (
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
variants: `search_term` (required, 1–500), **one** of `document_type` (a single corpus) or
|
|
38
|
+
`document_types` (an array — several corpora in one ranked call); `maximum_results` (1–20, def 15,
|
|
39
|
+
counted across the combined result set), `lines_in_each_result` (0–200, def 20; `0` = no per-hit
|
|
40
|
+
line cap). Corpora: `REST` (default) · `SDK` · `SKILLS` · `WIX_HEADLESS` · `BUSINESS_SOLUTIONS` ·
|
|
41
|
+
`VELO` · `WDS` · `BUILD_APPS` · `CLI` · `OVERVIEW`. `SKILLS` is the dedicated **recipe corpus** —
|
|
42
|
+
multi-step workflow pages a single-API search does not return; `OVERVIEW` is platform orientation
|
|
43
|
+
(which development approach, which API family).
|
|
44
|
+
|
|
45
|
+
**Search the corpora a question actually spans, in one call.** The service ranks them together and
|
|
46
|
+
interleaves the hits, so they compete on relevance instead of you guessing which to try first —
|
|
47
|
+
and one round trip replaces several. There's no fixed combination: pick by the question. Passing
|
|
48
|
+
both `document_type` and `document_types` is an error (*"Pass either document_type or
|
|
49
|
+
document_types, not both"*), and an unknown corpus name is rejected with the valid list.
|
|
50
|
+
|
|
51
|
+
Two variants — pick by what you're doing:
|
|
43
52
|
|
|
44
53
|
**`/docs/search/markdown` → read it (start here).** Returns JSON with a single `content` field
|
|
45
54
|
holding one LLM-ready markdown string (extract it with `jq -r '.content'`) where each hit is a
|
|
@@ -58,26 +67,44 @@ curl -sS -X POST 'https://www.wixapis.com/mcp-docs-search/v1/docs/search/markdow
|
|
|
58
67
|
| jq -r '.content' # no jq? → python3 -c 'import sys,json;print(json.load(sys.stdin)["content"])'
|
|
59
68
|
```
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
A question that spans a workflow and its individual calls asks for both corpora at once — the
|
|
71
|
+
recipe and the per-method contracts arrive in one ranked list:
|
|
62
72
|
|
|
63
73
|
```bash
|
|
64
74
|
curl -sS -X POST 'https://www.wixapis.com/mcp-docs-search/v1/docs/search/markdown' \
|
|
65
75
|
-H 'Content-Type: application/json' \
|
|
66
|
-
--data-raw '{"search_term":"end to end booking flow","
|
|
76
|
+
--data-raw '{"search_term":"end to end booking flow","document_types":["SKILLS","REST"],"maximum_results":6}' \
|
|
67
77
|
| jq -r '.content'
|
|
68
78
|
```
|
|
69
79
|
|
|
70
80
|
**`/docs/search` (JSON) → route on it.** Returns `{ results: [ { title, url, content,
|
|
71
|
-
relevance_score, … } ] }` — structured hits. Use it when you want to **pick/route
|
|
72
|
-
grab a hit's `url` to read that page (§2) or feed it to the schema query (§C).
|
|
73
|
-
|
|
81
|
+
relevance_score, kb_name, … } ] }` — structured hits. Use it when you want to **pick/route
|
|
82
|
+
programmatically**: grab a hit's `url` to read that page (§2) or feed it to the schema query (§C).
|
|
83
|
+
|
|
84
|
+
**`kb_name` tells you what each hit is** — every hit carries one, and the kind decides what you can
|
|
85
|
+
do with it: only a method page has a schema to pull (§C); an article or recipe is read as prose (§2).
|
|
86
|
+
|
|
87
|
+
The request can't make that split for you. **A corpus mixes kinds** — searching the API reference
|
|
88
|
+
returns method pages *and* the prose around them — and no corpus returns method pages alone. So
|
|
89
|
+
pick corpora with `document_types`, then separate the kinds on the way out with `kb_name`. Two
|
|
90
|
+
things not to do instead: don't infer the kind from the presence of a `url` (every hit has one,
|
|
91
|
+
articles and recipes included), and don't infer it from the corpus you asked for.
|
|
92
|
+
|
|
93
|
+
Most values name their kind (`…_METHODS_…` for method pages, `…_DOCS_…` for prose, `SKILLS_KB_ID`
|
|
94
|
+
for recipes) but not all do — the SDK's method index is `API_REFERENCE_SDK_KB_ID`. So list the
|
|
95
|
+
labels your search actually returned, then act on those, rather than pattern-matching the name or
|
|
96
|
+
hard-coding a list of ids.
|
|
74
97
|
|
|
75
98
|
```bash
|
|
99
|
+
# see what came back — label, title, url
|
|
76
100
|
curl -sS -X POST 'https://www.wixapis.com/mcp-docs-search/v1/docs/search' \
|
|
77
101
|
-H 'Content-Type: application/json' \
|
|
78
|
-
--data-raw '{"search_term":"
|
|
79
|
-
| jq -r '.results[] |
|
|
80
|
-
# no jq? → python3 -c 'import sys,json;[print(r["title"],r
|
|
102
|
+
--data-raw '{"search_term":"end to end booking flow","document_types":["REST","SKILLS","WIX_HEADLESS"],"maximum_results":10}' \
|
|
103
|
+
| jq -r '.results[] | "\(.kb_name)\t\(.title)\t\(.url)"'
|
|
104
|
+
# no jq? → python3 -c 'import sys,json;[print(r.get("kb_name"),r["title"],r.get("url")) for r in json.load(sys.stdin)["results"]]'
|
|
105
|
+
|
|
106
|
+
# then take the kind you need, by the label you just saw
|
|
107
|
+
… | jq -r '.results[] | select(.kb_name == "REST_METHODS_KB_ID") | "\(.title)\t\(.url)"'
|
|
81
108
|
```
|
|
82
109
|
|
|
83
110
|
**B. Browse the docs tree as a menu.** Two ways: the **structured browse endpoint** for the
|