@wix/agent-skills 1.31.2 → 1.32.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.
Files changed (40) hide show
  1. package/README.md +4 -12
  2. package/package.json +1 -1
  3. package/skills/wix-app/SKILL.md +39 -15
  4. package/skills/wix-app/references/BACKEND_API.md +1 -1
  5. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +11 -8
  6. package/skills/wix-app/references/SDK_MODULE_MAP.md +1 -1
  7. package/skills/wix-app/references/STORES_VERSIONING.md +24 -2
  8. package/skills/wix-app/references/dashboard-page/DATA_SOURCES.md +9 -13
  9. package/skills/wix-app/references/dashboard-page/IMAGE_RENDERING.md +61 -0
  10. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +54 -64
  11. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +14 -21
  12. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +37 -30
  13. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +43 -29
  14. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +40 -39
  15. package/skills/wix-app/references/editor-react-component/PARTS.md +8 -6
  16. package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +22 -2
  17. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +39 -16
  18. package/skills/wix-app/scripts/sdk-curated.json +45 -0
  19. package/skills/wix-app/scripts/sdk-lookup.cjs +1180 -0
  20. package/skills/wix-base44-connector/SKILL.md +4 -2
  21. package/skills/wix-base44-connector/scripts/utils.cjs +5 -2
  22. package/skills/wix-headless-kit/SKILL.md +4 -1
  23. package/skills/wix-headless-kit/guides/api-run.md +14 -20
  24. package/skills/wix-headless-kit/install/pins.json +1 -1
  25. package/skills/wix-manage/SKILL.md +30 -8
  26. package/skills/wix-manage/references/automations/actions.md +134 -0
  27. package/skills/wix-manage/references/automations/activation-status.md +64 -48
  28. package/skills/wix-manage/references/automations/api-catalog.md +213 -77
  29. package/skills/wix-manage/references/automations/automation-model.md +199 -0
  30. package/skills/wix-manage/references/automations/bracket-expressions.md +132 -0
  31. package/skills/wix-manage/references/automations/build-and-manage-automations.md +305 -26
  32. package/skills/wix-manage/references/automations/triggers.md +190 -0
  33. package/skills/wix-manage/references/collaborators/manage-site-collaborators.md +128 -0
  34. package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +3 -1
  35. package/skills/wix-manage/references/forms/read-forms-and-submissions.md +277 -0
  36. package/skills/wix-manage/references/forms/update-form.md +5 -0
  37. package/skills/wix-manage/references/seo/manage-url-redirects.md +33 -4
  38. package/skills/wix-manage/references/sites/create-headless-site.md +14 -5
  39. package/skills/wix-manage/references/sites/upload-static-site.md +373 -302
  40. package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +3 -3
@@ -0,0 +1,1180 @@
1
+ #!/usr/bin/env node
2
+ // Answers "which SDK call reads or writes this entity" for every entity in one
3
+ // call, from the Wix SDK packages installed in the current project.
4
+ //
5
+ // node sdk-lookup.cjs contacts orders [--intent search|read|write|event|all] [--json]
6
+ //
7
+ // Per entity it prints the namespace to use and why, the import, the methods
8
+ // for the intent with their call shape, permission and required fields, the
9
+ // entity type's declaration (file:line) for checking fields, and any curated
10
+ // rule from sdk-curated.json.
11
+ //
12
+ // Each @wix/auto_sdk_* package is one record, derived from the package's .d.ts
13
+ // files: the JSDoc tags (@fqn, @permissionId, @permissionScopeId, ...), the
14
+ // public call signatures and the search and query specs, which every
15
+ // generated package carries.
16
+ //
17
+ // Exit codes: 0 every entity found · 1 an entity matched nothing (near matches
18
+ // printed) · 3 an entity's package is not installed (install command printed)
19
+ // · 2 usage · 4 an installed package is in a form the lookup does not read.
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ class LookupError extends Error {}
24
+
25
+ const INTENTS = {
26
+ search: ['search', 'query-builder', 'query', 'list'],
27
+ read: ['query-builder', 'query', 'search', 'list', 'get', 'count'],
28
+ write: ['create', 'update', 'upsert', 'delete', 'bulk'],
29
+ event: [],
30
+ all: null,
31
+ };
32
+ // `all` lists every method, the search ones first, in this order.
33
+ const ALL_ORDER = [...INTENTS.search, 'get', 'count', ...INTENTS.write];
34
+ const searching = (intent) => intent === 'search' || intent === 'all';
35
+
36
+ // --- package resolution ------------------------------------------------------
37
+
38
+ function activatePnp(from) {
39
+ for (let dir = from; ; dir = path.dirname(dir)) {
40
+ const pnp = path.join(dir, '.pnp.cjs');
41
+ if (fs.existsSync(pnp)) {
42
+ try {
43
+ require(pnp).setup();
44
+ } catch {
45
+ // Not fatal: node_modules resolution below may still work.
46
+ }
47
+ return;
48
+ }
49
+ if (path.dirname(dir) === dir) return;
50
+ }
51
+ }
52
+
53
+ function resolveDir(name, from) {
54
+ try {
55
+ return path.dirname(require.resolve(`${name}/package.json`, { paths: [from] }));
56
+ } catch {
57
+ for (let dir = from; ; dir = path.dirname(dir)) {
58
+ const candidate = path.join(dir, 'node_modules', ...name.split('/'));
59
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
60
+ if (path.dirname(dir) === dir) return null;
61
+ }
62
+ }
63
+ }
64
+
65
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
66
+ // contacts-v-5 → contactsV5, order-transactions → orderTransactions
67
+ const camel = (s) => s.replace(/-v-(\d+)$/, 'V$1').replace(/-(\w)/g, (_, c) => c.toUpperCase());
68
+ const isAuto = (name) => name.startsWith('@wix/auto_sdk_');
69
+
70
+ function wixDirsAbove(from) {
71
+ const out = [];
72
+ for (let dir = from; ; dir = path.dirname(dir)) {
73
+ const wix = path.join(dir, 'node_modules', '@wix');
74
+ if (fs.existsSync(wix)) out.push(wix);
75
+ if (path.dirname(dir) === dir) return out;
76
+ }
77
+ }
78
+
79
+ // Finds every public SDK package (one that depends on @wix/auto_sdk_*
80
+ // packages) and every auto package, with the namespace each is exported as.
81
+ function findPackages(project) {
82
+ const wrappers = new Map(); // name -> dir
83
+ const consider = (name, from) => {
84
+ if (wrappers.has(name) || isAuto(name) || !name.startsWith('@wix/')) return;
85
+ const dir = resolveDir(name, from);
86
+ if (!dir) return;
87
+ const deps = Object.keys(readJson(path.join(dir, 'package.json')).dependencies ?? {});
88
+ if (deps.some(isAuto)) wrappers.set(name, dir);
89
+ };
90
+ const pj = path.join(project, 'package.json');
91
+ if (fs.existsSync(pj)) {
92
+ const { dependencies = {}, devDependencies = {} } = readJson(pj);
93
+ for (const name of Object.keys({ ...dependencies, ...devDependencies })) consider(name, project);
94
+ }
95
+ for (const wix of wixDirsAbove(project)) {
96
+ for (const d of fs.readdirSync(wix)) consider(`@wix/${d}`, project);
97
+ }
98
+
99
+ const autos = new Map(); // name -> { dir, exportedAs: [] }
100
+ for (const [wrapper, dir] of wrappers) {
101
+ const deps = Object.keys(readJson(path.join(dir, 'package.json')).dependencies ?? {});
102
+ const barrel = ['build/cjs/index.d.ts', 'build/es/index.d.mts']
103
+ .map((f) => path.join(dir, f))
104
+ .find((f) => fs.existsSync(f));
105
+ const src = barrel ? fs.readFileSync(barrel, 'utf8') : '';
106
+ for (const dep of deps.filter(isAuto)) {
107
+ const depDir = resolveDir(dep, dir);
108
+ if (!depDir) continue;
109
+ const entry = autos.get(dep) ?? { dir: depDir, exportedAs: [] };
110
+ const m = src.match(new RegExp(`import \\* as (\\w+) from '${dep}';\\s*export \\{ \\1 as (\\w+) \\}`));
111
+ if (m) entry.exportedAs.push({ from: wrapper, namespace: m[2] });
112
+ // The export file names the package in a form this regex does not read
113
+ // (`export * as ns from '…'`): stop, or it is labeled "not exported". A bare
114
+ // `import '…'` (app-management's custom-charges) exports nothing.
115
+ else if (new RegExp(`(export|import \\* as \\w+)[^;']*from '${dep}'`).test(src)) {
116
+ throw new LookupError(`${wrapper} exports ${dep} in a form sdk-lookup does not read (${path.relative(dir, barrel)}); its namespace is unknown, so do not trust this lookup`);
117
+ }
118
+ autos.set(dep, entry);
119
+ }
120
+ }
121
+ // An auto package on disk that no installed wrapper re-exports is either a
122
+ // dependency of another package (its own wrapper is not installed: name it
123
+ // and the install command) or an SPI/internal package of an installed
124
+ // wrapper (not importable from the wrapper's root).
125
+ for (const wix of wixDirsAbove(project)) {
126
+ for (const d of fs.readdirSync(wix).filter((d) => d.startsWith('auto_sdk_'))) {
127
+ const name = `@wix/${d}`;
128
+ if (!autos.has(name)) autos.set(name, { dir: path.join(wix, d), exportedAs: [] });
129
+ }
130
+ }
131
+ for (const [name, entry] of autos) {
132
+ if (entry.exportedAs.length) continue;
133
+ const [, vertical, ns] = name.match(/^@wix\/auto_sdk_([^_]+)_(.+)$/) ?? [];
134
+ const wrapper = vertical ? `@wix/${vertical}` : null;
135
+ if (wrapper && !wrappers.has(wrapper)) {
136
+ entry.exportedAs.push({ from: wrapper, namespace: camel(ns), installed: false });
137
+ }
138
+ }
139
+ return { wrappers, autos };
140
+ }
141
+
142
+ // --- deriving a record from .d.ts --------------------------------------------
143
+
144
+ // Reads one `declare function name(...): Ret;` starting at `at`, balancing
145
+ // brackets so callback parameters and generic return types stay whole.
146
+ function readDeclaration(src, at) {
147
+ let depth = 0;
148
+ let i = src.indexOf('(', at);
149
+ const paramsStart = i + 1;
150
+ let paramsEnd = -1;
151
+ for (; i < src.length; i++) {
152
+ const c = src[i];
153
+ if (c === '(' || c === '<' || c === '{' || c === '[') depth++;
154
+ else if (c === ')' || c === '>' || c === '}' || c === ']') {
155
+ if (src[i - 1] === '=' && c === '>') continue; // arrow
156
+ depth--;
157
+ if (depth === 0 && paramsEnd < 0 && c === ')') {
158
+ paramsEnd = i;
159
+ if (src[i + 1] !== ':') break;
160
+ }
161
+ } else if (c === ';' && depth === 0 && paramsEnd >= 0) break;
162
+ }
163
+ const params = src.slice(paramsStart, paramsEnd);
164
+ const ret = src.slice(paramsEnd + 1, i).replace(/^:\s*/, '').trim();
165
+ return { params, ret, end: i };
166
+ }
167
+
168
+ const tags = (doc, tag) =>
169
+ [...doc.matchAll(new RegExp(`@${tag}(?:\\s+([^\\n*]*))?`, 'g'))].map((m) => (m[1] ?? '').trim());
170
+
171
+ // The first sentence of the JSDoc right before `at`: what the entity is ("An
172
+ // order represents a completed purchase from a buyer."), which tells apart
173
+ // the packages that export the same namespace. Null when there is none.
174
+ // One JSDoc block as one line of text, links removed.
175
+ const jsdocLine = (doc) =>
176
+ doc.split('\n').map((l) => l.replace(/^\s*\*\s?/, '').trim()).join(' ').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim();
177
+
178
+ function descriptionBefore(src, at) {
179
+ const end = src.lastIndexOf('*/', at);
180
+ if (end < 0 || src.slice(end + 2, at).trim()) return null;
181
+ const text = src
182
+ .slice(src.lastIndexOf('/**', end) + 3, end)
183
+ .split('\n')
184
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
185
+ .join(' ')
186
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
187
+ .replace(/\s+/g, ' ')
188
+ .trim();
189
+ if (!text || text.startsWith('@')) return null;
190
+ return text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text.split(' @')[0];
191
+ }
192
+
193
+ // The `> **Note:**` blocks of a method's JSDoc, the same text as its docs page.
194
+ // They carry who may call it ("requires visitor or member authentication"),
195
+ // which @applicableIdentity does not: getCurrentMember is tagged APP alone.
196
+ const UPDATE_RULE = /\bto (clear|remove) (a|the)\b[^.]*\b(value|field)|\b(replaces|overwrites) the (whole|entire) array\b|\bpartial update\b|\bonly the (requested|provided|specified|listed) fields\b/i;
197
+
198
+ function notesOf(doc) {
199
+ const lines = doc.split('\n').map((l) => l.replace(/^\s*\*\s?/, ''));
200
+ const notes = [];
201
+ for (let i = 0; i < lines.length; i++) {
202
+ const head = /^>\s*\*\*Notes?:\*\*/.exec(lines[i]);
203
+ if (!head) continue;
204
+ const text = [lines[i].slice(head[0].length)];
205
+ while (i + 1 < lines.length && lines[i + 1].startsWith('>')) text.push(lines[++i].slice(1));
206
+ const note = text.join(' ').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim();
207
+ if (note) notes.push(note);
208
+ }
209
+ // An update's field rules are plain description text, not a Note block:
210
+ // without "To clear a field's value, set … `""`" an agent sends undefined,
211
+ // which the PATCH drops, and the old value stays.
212
+ const tagAt = lines.findIndex((l) => l.startsWith('@'));
213
+ const desc = lines.slice(0, tagAt < 0 ? lines.length : tagAt).filter((l) => !l.startsWith('>')).join(' ');
214
+ for (const s of desc.split(/(?<=\.)\s+(?=[A-Z`])/)) {
215
+ if (!UPDATE_RULE.test(s)) continue;
216
+ const rule = s.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim();
217
+ if (!notes.includes(rule)) notes.push(rule);
218
+ }
219
+ return notes;
220
+ }
221
+
222
+ function kindOf(name, params, ret) {
223
+ if (/^on[A-Z]/.test(name)) return 'event';
224
+ if (/^search/.test(name)) return 'search';
225
+ if (/^query/.test(name)) {
226
+ if (/QueryBuilder/.test(ret)) return 'query-builder';
227
+ return /^\s*(query|options)\??:/.test(params) || /Query\b/.test(params) ? 'query' : 'other';
228
+ }
229
+ for (const k of ['list', 'get', 'count', 'create', 'update', 'upsert', 'bulk']) {
230
+ if (name.startsWith(k)) return k;
231
+ }
232
+ if (/^(delete|remove)/.test(name)) return 'delete';
233
+ return 'other';
234
+ }
235
+
236
+ // Drops the noise the generator puts in signatures, so a line stays readable.
237
+ function shorten(type, max = 90) {
238
+ let t = type
239
+ .replace(/NonNullablePaths<\s*([\w.]+)\s*,[^>]*?,\s*\d+\s*>/g, '$1')
240
+ .replace(/\s+&\s+\{[\s\S]*$/, '')
241
+ .replace(/\s+/g, ' ')
242
+ .trim();
243
+ const open = (t.match(/</g) ?? []).length - (t.match(/>/g) ?? []).length;
244
+ if (open > 0) t += '>'.repeat(open);
245
+ if (t.length > max) t = `${t.slice(0, max - 1)}…`;
246
+ return t;
247
+ }
248
+
249
+ function requiredOf(params) {
250
+ const out = new Set();
251
+ for (const m of params.matchAll(/NonNullablePaths<\s*[\w.]+\s*,([^>]*?),\s*\d+\s*>/g)) {
252
+ for (const f of m[1].matchAll(/`([^`]+)`/g)) out.add(f[1]);
253
+ }
254
+ for (const m of params.matchAll(/(?:^|,)\s*(revision)\s*:/g)) out.add(m[1]);
255
+ return [...out];
256
+ }
257
+
258
+ function paramList(params) {
259
+ return splitParams(params).map((p) => {
260
+ const m = p.match(/^(\w+\??)\s*:\s*([\s\S]*)$/);
261
+ return m ? `${m[1]}: ${shorten(m[2], 40)}` : shorten(p, 40);
262
+ });
263
+ }
264
+
265
+ // Splits a parameter list on its top-level commas.
266
+ function splitParams(params) {
267
+ const out = [];
268
+ let depth = 0;
269
+ let cur = '';
270
+ for (const c of params) {
271
+ if ('(<{['.includes(c)) depth++;
272
+ if (')>}]'.includes(c)) depth--;
273
+ if (c === ',' && depth === 0) {
274
+ out.push(cur);
275
+ cur = '';
276
+ } else cur += c;
277
+ }
278
+ if (cur.trim()) out.push(cur);
279
+ return out.map((p) => p.trim()).filter(Boolean);
280
+ }
281
+
282
+ const pascal = (s) => s.replace(/(^|[_-])(\w)/g, (_, __, c) => c.toUpperCase());
283
+
284
+ // The body of `interface Name {…}` or `type Name = {…}`, skipping an empty `{}`.
285
+ function typeBody(sources, name) {
286
+ const re = new RegExp(`^(?:interface ${name}\\b[^{\\n]*|type ${name}\\s*=\\s*)\\{`, 'gm');
287
+ for (const src of sources) {
288
+ for (const m of src.matchAll(re)) {
289
+ let depth = 0;
290
+ let i = m.index + m[0].length - 1;
291
+ for (; i < src.length; i++) {
292
+ if (src[i] === '{') depth++;
293
+ else if (src[i] === '}' && --depth === 0) break;
294
+ }
295
+ const body = src.slice(m.index + m[0].length, i);
296
+ if (body.trim()) return body;
297
+ }
298
+ }
299
+ return null;
300
+ }
301
+
302
+ // Top-level members of a type body: [{ name, type }], comments dropped.
303
+ function membersOf(body) {
304
+ const clean = body.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\$\{[^}]*\}/g, '');
305
+ const out = [];
306
+ let depth = 0;
307
+ for (const line of clean.split('\n')) {
308
+ const m = depth === 0 && line.match(/^\s*(\w+)\??\s*:\s*(.*)$/);
309
+ if (m) out.push({ name: m[1], type: m[2] });
310
+ else if (out.length && depth > 0) out[out.length - 1].type += `\n${line}`;
311
+ depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
312
+ }
313
+ return out;
314
+ }
315
+
316
+ // `{ a, b: { c, d }, e: [{ f }] }`: the keys of a type, two levels deep.
317
+ // Named types are followed, except the entity type (its fields are in `type`).
318
+ function typeShape(sources, name, skip, depth = 0) {
319
+ const body = typeBody(sources, name);
320
+ return body ? shapeOfBody(sources, body, skip, depth) : null;
321
+ }
322
+
323
+ function shapeOfBody(sources, body, skip, depth) {
324
+ const keys = membersOf(body).map(({ name, type }) => {
325
+ if (depth >= 2) return name;
326
+ const t = type.trim();
327
+ const list = /\[\]\s*;?\s*$/.test(t) || /^Array</.test(t);
328
+ let inner = null;
329
+ if (t.startsWith('{')) inner = shapeOfBody(sources, t.slice(1, t.lastIndexOf('}')), skip, depth + 1);
330
+ else {
331
+ const ref = t.match(/^(?:Array<)?(\w+)/)?.[1];
332
+ if (ref && !skip.has(ref) && /^[A-Z]/.test(ref)) inner = typeShape(sources, ref, skip, depth + 1);
333
+ }
334
+ if (!inner) {
335
+ const ref = t.match(/^(\w+)(\[\])?/);
336
+ return ref && skip.has(ref[1]) ? `${name}: ${ref[1]}${ref[2] ?? ''}` : name;
337
+ }
338
+ return `${name}: ${list ? `[${inner}]` : inner}`;
339
+ });
340
+ if (!keys.length) return null;
341
+ return `{ ${keys.length > 8 ? [...keys.slice(0, 8), '…'].join(', ') : keys.join(', ')} }`;
342
+ }
343
+
344
+ // The declared type of a dotted field path (`contact.firstName`) in a type
345
+ // body, with `| null` dropped; a builder's `category.id` is the type's `_id`.
346
+ function fieldType(sources, body, dotted) {
347
+ const parts = dotted.split('.');
348
+ for (let i = 0; body && i < parts.length; i++) {
349
+ const m = membersOf(body).find((x) => x.name === parts[i] || x.name === `_${parts[i]}`);
350
+ if (!m) return null;
351
+ const t = m.type.trim().replace(/;$/, '').replace(/\s*\|\s*(null|undefined)\b/g, '').trim();
352
+ if (i === parts.length - 1) return t;
353
+ body = t.startsWith('{') ? t.slice(1, t.lastIndexOf('}')) : typeBody(sources, t.match(/^(\w+)/)?.[1] ?? '');
354
+ }
355
+ return null;
356
+ }
357
+
358
+ function deriveRecord(name, dir) {
359
+ const pj = readJson(path.join(dir, 'package.json'));
360
+ const dep = pj.wix?.sdkDependency ?? {};
361
+ const cjs = path.join(dir, 'build', 'cjs');
362
+ const files = fs.existsSync(cjs)
363
+ ? fs.readdirSync(cjs).filter((f) => f.endsWith('.d.ts') && f !== 'meta.d.ts')
364
+ : [];
365
+ const methods = new Map();
366
+ const rawParams = new Map(); // name -> the declaration's full parameter text
367
+ const events = [];
368
+ let preview = 0;
369
+ let typeAt = null;
370
+ const entityType = dep.fqdn ? pascal(dep.fqdn.split('.').pop()) : null;
371
+ const sources = [];
372
+
373
+ for (const f of files) {
374
+ const file = path.join(cjs, f);
375
+ const src = fs.readFileSync(file, 'utf8');
376
+ sources.push(src);
377
+ if (entityType && !typeAt) {
378
+ const m = new RegExp(`^interface ${entityType}\\b`, 'm').exec(src);
379
+ if (m) typeAt = { name: entityType, file, line: src.slice(0, m.index).split('\n').length, description: descriptionBefore(src, m.index) };
380
+ }
381
+ const re = /\/\*\*((?:(?!\*\/)[\s\S])*?)\*\/\s*declare function (\w+?)(?:\$\d+)?\s*\(/g;
382
+ let m;
383
+ while ((m = re.exec(src))) {
384
+ const [, doc, fn] = m;
385
+ const { params, ret } = readDeclaration(src, m.index + m[0].length - 1);
386
+ if (/^\s*httpClient\s*:/.test(params)) continue; // the client-bound wrapper
387
+ if (/@hidden\b|@internal\b/.test(doc)) continue;
388
+ const kind = kindOf(fn, params, ret);
389
+ if (kind === 'event') {
390
+ if (!events.some((e) => e.name === fn)) {
391
+ events.push({ name: fn, scopes: [...new Set(tags(doc, 'permissionScopeId'))] });
392
+ }
393
+ continue;
394
+ }
395
+ const maturity = tags(doc, 'documentationMaturity')[0];
396
+ if (maturity === 'preview') preview++;
397
+ const prev = methods.get(fn);
398
+ const record = {
399
+ name: fn,
400
+ kind,
401
+ fqn: tags(doc, 'fqn')[0] ?? prev?.fqn,
402
+ params: paramList(params),
403
+ returns: shorten(ret),
404
+ permission: tags(doc, 'permissionId')[0] ?? prev?.permission,
405
+ // Only events carry @permissionScopeId today; methods get it if the SDK adds it.
406
+ scopes: [...new Set([...(prev?.scopes ?? []), ...tags(doc, 'permissionScopeId')])],
407
+ identities: [...new Set([...(prev?.identities ?? []), ...tags(doc, 'applicableIdentity')])],
408
+ admin: /@adminMethod\b/.test(doc) || !!prev?.admin,
409
+ notes: [...new Set([...(prev?.notes ?? []), ...notesOf(doc)])],
410
+ required: [...new Set([...(prev?.required ?? []), ...requiredOf(params)])],
411
+ maturity: maturity ? maturity.toUpperCase() : prev?.maturity ?? 'GA',
412
+ deprecated: /@deprecated\b/.test(doc)
413
+ ? { replacedBy: tags(doc, 'replacedBy')[0] ?? null, removal: tags(doc, 'targetRemovalDate')[0] ?? null }
414
+ : prev?.deprecated ?? null,
415
+ // Where to read the parameter and return types: the public Signature
416
+ // below replaces this when the package has one.
417
+ decl: prev?.decl ?? { file, line: src.slice(0, m.index + m[0].lastIndexOf('declare')).split('\n').length },
418
+ };
419
+ // Overloads: keep the richer signature (a query builder beats `other`).
420
+ if (!prev || (prev.kind === 'other' && kind !== 'other')) {
421
+ methods.set(fn, record);
422
+ rawParams.set(fn, params);
423
+ } else methods.set(fn, { ...prev, ...Object.fromEntries(Object.entries(record).filter(([, v]) => v != null)), kind: prev.kind, params: prev.params, returns: prev.returns });
424
+ }
425
+ }
426
+
427
+ // The public call signature is `interface <Name>Signature { (params): Ret }`
428
+ // in index.d.ts. The `declare function` in index.typings.d.ts is the REST
429
+ // request shape (`search: CursorSearch` where the public call takes
430
+ // `ContactSearch`, and `options` required where it is optional), so the
431
+ // Signature wins whenever it exists.
432
+ for (const meth of methods.values()) {
433
+ const sig = `${meth.name[0].toUpperCase()}${meth.name.slice(1)}Signature`;
434
+ for (const [i, src] of sources.entries()) {
435
+ const m = new RegExp(`interface ${sig}\\s*\\{[\\s\\S]*?\\n\\s{4}\\(`).exec(src);
436
+ if (!m) continue;
437
+ const { params, ret } = readDeclaration(src, m.index + m[0].length - 1);
438
+ meth.decl = { file: path.join(cjs, files[i]), line: src.slice(0, m.index).split('\n').length };
439
+ meth.params = paramList(params);
440
+ rawParams.set(meth.name, params);
441
+ meth.returns = shorten(ret);
442
+ meth.required = [...new Set([...meth.required, ...requiredOf(params)])];
443
+ break;
444
+ }
445
+ }
446
+
447
+ // <Entity>SearchSpec / <Entity>QuerySpec declare what a search or query
448
+ // method accepts: the free-text fields and, per operator group, the fields
449
+ // and sort. A closed list, shipped in every generated package that has one.
450
+ const spec = (kind) => {
451
+ const names = entityType ? [`${entityType}${kind}Spec`] : [];
452
+ for (const src of sources) {
453
+ const all = [...src.matchAll(new RegExp(`interface (\\w+)${kind}Spec extends ${kind}Spec\\b`, 'g'))].map((x) => `${x[1]}${kind}Spec`);
454
+ const name = names.find((n) => all.includes(n)) ?? (all.length === 1 ? all[0] : null);
455
+ if (name) {
456
+ const fields = parseSpec(src, name);
457
+ return fields && { entity: name.slice(0, -`${kind}Spec`.length), fields };
458
+ }
459
+ }
460
+ return null;
461
+ };
462
+ const searchSpec = spec('Search');
463
+ const querySpec = spec('Query');
464
+ // The spec is one entity's (`V3ProductSearchSpec` → V3Product), so a query
465
+ // or search returning another type in the same namespace
466
+ // (services.queryPolicies) does not get it. A builder is generated per
467
+ // entity, so it always does.
468
+ const returnsEntity = (meth, entity) => {
469
+ const body = typeBody(sources, meth.returns.match(/Promise<(\w+)/)?.[1] ?? '');
470
+ const list = new RegExp(`^(?:Array<\\s*${entity}\\b|${entity}\\s*\\[\\])`);
471
+ return !body || membersOf(body).some(({ type }) => list.test(type.trim()));
472
+ };
473
+ for (const meth of methods.values()) {
474
+ const s = meth.kind === 'search' ? searchSpec : /^query/.test(meth.kind) ? querySpec : null;
475
+ if (!s || (meth.kind !== 'query-builder' && !returnsEntity(meth, s.entity))) continue;
476
+ if (Object.keys(s.fields).length) meth.filterable = s.fields;
477
+ else meth.filterUnlisted = true;
478
+ }
479
+
480
+ const http = httpOf(path.join(cjs, 'meta.js'));
481
+ for (const meth of methods.values()) if (http[meth.name]) meth.http = http[meth.name];
482
+
483
+ // A search method takes free text only when its search object has a
484
+ // `search` member (SearchDetails); some search methods take a filter only.
485
+ for (const meth of methods.values()) {
486
+ if (meth.kind !== 'search') continue;
487
+ if (meth.filterable) {
488
+ meth.freeText = Object.values(meth.filterable).some((f) => f.search);
489
+ continue;
490
+ }
491
+ const type = meth.params[0]?.match(/:\s*(\w+)/)?.[1];
492
+ for (const src of sources) {
493
+ const m = new RegExp(`(?:interface ${type}\\b[^{]*|type ${type}\\s*=\\s*)\\{([\\s\\S]*?)\\n\\}`).exec(src);
494
+ if (!m) continue;
495
+ meth.freeText = /^\s{4}search\?\s*:/m.test(m[1]);
496
+ break;
497
+ }
498
+ }
499
+
500
+ // A query builder's own methods decide what one builder call can express:
501
+ // the generated builders chain clauses with AND and most have no or().
502
+ for (const meth of methods.values()) {
503
+ if (meth.kind !== 'query-builder') continue;
504
+ const builder = meth.returns.match(/(\w+QueryBuilder)/)?.[1];
505
+ for (const src of sources) {
506
+ const m = new RegExp(`interface ${builder}\\b[^{]*\\{([\\s\\S]*?)\\n\\}`).exec(src);
507
+ if (!m) continue;
508
+ // Each member is `op: (propertyName: 'a' | 'b', value) => Builder`: the
509
+ // literals are the closed list of fields that operator accepts.
510
+ const ops = {};
511
+ for (const line of m[1].split('\n')) {
512
+ const mm = line.match(/^\s{4}(\w+)\s*:\s*\(/);
513
+ if (!mm) continue;
514
+ const params = line.slice(0, line.lastIndexOf('=>'));
515
+ const fields = [...params.matchAll(/'([^']+)'/g)].map((x) => x[1]);
516
+ ops[mm[1]] = /propertyNames?\s*:\s*string/.test(params) ? ['*'] : fields;
517
+ }
518
+ meth.builder = ops;
519
+ break;
520
+ }
521
+ // Most builders also take one query object — `queryX({ filter, sort,
522
+ // cursorPaging })` — declared as the @hidden `typedQueryX`. Its filter is
523
+ // a WQL object, so it has $or where the builder has no or().
524
+ const typed = `typed${meth.name[0].toUpperCase()}${meth.name.slice(1)}`;
525
+ for (const src of sources) {
526
+ const m = new RegExp(`declare function ${typed}\\s*\\(`).exec(src);
527
+ if (!m) continue;
528
+ const { params, ret } = readDeclaration(src, m.index + m[0].length - 1);
529
+ const form = { params: paramList(params), returns: shorten(ret) };
530
+ const skip = new Set([entityType]);
531
+ const arg = params.match(/^\s*(\w+)\??\s*:\s*([A-Z]\w*)/);
532
+ const argShape = arg && typeShape(sources, arg[2], skip);
533
+ if (argShape) form.arg = `${arg[1]}: ${argShape}`;
534
+ const retType = form.returns.match(/Promise<(\w+)/)?.[1];
535
+ const result = retType && typeShape(sources, retType, skip);
536
+ if (result) form.result = result;
537
+ // `cursorPaging` or `paging`, depending on the method.
538
+ form.keys = membersOf(typeBody(sources, arg?.[2] ?? '') ?? '').map((x) => x.name);
539
+ meth.queryForm = form;
540
+ break;
541
+ }
542
+ }
543
+
544
+ // The fields a search term can go in without a free-text search, declared
545
+ // `string` on the entity, ids and numbers in a string (`price.amount`,
546
+ // `taxRate`, `revision`) left out. `textFields`: those that list
547
+ // $startsWith. `textFieldsAny`: those under `operators: '*'` (every
548
+ // operator: orders' `number` takes $startsWith on a real site, although its
549
+ // docs page lists fewer). [] means the package lists the operators and none
550
+ // fits; no `textFields` means the package does not list them.
551
+ const entity = searchSpec?.entity ?? querySpec?.entity ?? entityType;
552
+ const entityBody = entity && typeBody(sources, entity);
553
+ // The fqdn can name the entity another way than its interface
554
+ // (wix.forms.v4.submission declares FormSubmission): the spec names it.
555
+ if (!typeAt && entity && entity !== entityType) {
556
+ const re = new RegExp(`^interface ${entity}\\b`, 'm');
557
+ sources.some((src, i) => {
558
+ const m = re.exec(src);
559
+ if (m) typeAt = { name: entity, file: path.join(cjs, files[i]), line: src.slice(0, m.index).split('\n').length, description: descriptionBefore(src, m.index) };
560
+ return m;
561
+ });
562
+ }
563
+ const notText = (f) => /(^|\.)(_?id|amount|value|revision)$|(Id|Count|Rate)$/.test(f);
564
+ const isText = (f) => !notText(f) && (!entityBody || fieldType(sources, entityBody, f) === 'string');
565
+ for (const meth of methods.values()) {
566
+ if (!['search', 'query', 'query-builder'].includes(meth.kind)) continue;
567
+ const sw = meth.builder?.startsWith;
568
+ let fields;
569
+ let any = [];
570
+ if (meth.filterable) {
571
+ const rows = Object.entries(meth.filterable);
572
+ fields = rows.filter(([, f]) => f.ops.includes('$startsWith')).map(([k]) => k);
573
+ any = rows.filter(([k, f]) => f.ops.includes('*') && !fields.includes(k)).map(([k]) => k);
574
+ if (sw && !sw.includes('*')) fields.push(...sw.filter((f) => !fields.includes(f)));
575
+ } else if (meth.builder && !meth.filterUnlisted && !sw?.includes('*')) fields = sw ?? [];
576
+ if (!fields) continue;
577
+ meth.textFields = fields.filter(isText);
578
+ any = any.filter((f) => !meth.textFields.includes(f) && isText(f));
579
+ if (any.length) meth.textFieldsAny = any;
580
+ }
581
+
582
+ // A query or list method can take free text in its options, with no search
583
+ // method in the namespace: queryMembers(options) has `options.search:
584
+ // { expression, fields }`, whose fields default to one (`profile.nickname`).
585
+ for (const meth of methods.values()) {
586
+ if (!['query', 'query-builder', 'list'].includes(meth.kind)) continue;
587
+ for (const p of splitParams(rawParams.get(meth.name) ?? '')) {
588
+ const m = p.match(/^(\w+)\??\s*:\s*([A-Z]\w*)/);
589
+ const search = m && membersOf(typeBody(sources, m[2]) ?? '').find((x) => x.name === 'search');
590
+ const type = search?.type.match(/^([A-Z]\w*)/)?.[1];
591
+ const body = type && typeBody(sources, type);
592
+ if (!body || !membersOf(body).some((x) => x.name === 'expression')) continue;
593
+ const doc = body.match(/\/\*\*((?:(?!\*\/)[\s\S])*)\*\/\s*fields\??\s*:/)?.[1] ?? '';
594
+ const [list, dflt] = doc.split(/Default:/);
595
+ meth.optionsSearch = {
596
+ param: m[1],
597
+ fields: [...list.matchAll(/`([\w.]+)`/g)].map((x) => x[1]),
598
+ default: dflt?.match(/`([\w.]+)`/)?.[1] ?? null,
599
+ max: Number(doc.match(/@maxSize (\d+)/)?.[1]) || null,
600
+ };
601
+ break;
602
+ }
603
+ }
604
+
605
+ // The keys of a search, query or list method's argument and response, so a
606
+ // caller does not have to find `ContactSearch` (an empty `{}` placeholder in
607
+ // index.typings.d.ts, the real `type ContactSearch = {` elsewhere).
608
+ // A fieldsets option decides which fields come back: queryMembers defaults to
609
+ // PUBLIC, without loginEmail or contact, so a table of names and emails
610
+ // renders empty columns unless it asks for FULL. The enum's JSDoc lists the
611
+ // fields of each set.
612
+ for (const meth of methods.values()) {
613
+ for (const p of splitParams(rawParams.get(meth.name) ?? '')) {
614
+ const t = p.match(/^\w+\??\s*:\s*([A-Z]\w*)/)?.[1];
615
+ const f = t && /(?:\/\*\*((?:(?!\*\/)[\s\S])*?)\*\/\s*)?fieldsets\??\s*:\s*([A-Z]\w*?)(?:WithLiterals)?\[\]/.exec(typeBody(sources, t) ?? '');
616
+ const enumBody = f && sources.map((s) => new RegExp(`declare enum ${f[2]} \\{([\\s\\S]*?)\\n\\}`).exec(s)?.[1]).find(Boolean);
617
+ const values = [...(enumBody ?? '').matchAll(/\/\*\*((?:(?!\*\/)[\s\S])*?)\*\/\s*(\w+) =/g)]
618
+ .map(([, doc, name]) => {
619
+ const text = jsdocLine(doc);
620
+ // The top-level fields the set returns, from its "Includes …" sentence
621
+ // only (PUBLIC's next sentence names fields it returns as UNKNOWN).
622
+ const includes = /\ball fields\b/i.test(text)
623
+ ? 'all'
624
+ : [...(text.match(/\bIncludes ([^.]*)/)?.[1] ?? '').matchAll(/`([\w.]+)`/g)].map((x) => x[1]);
625
+ return { name, fields: text, includes };
626
+ });
627
+ if (!values.length) continue;
628
+ meth.fieldsets = {
629
+ default: (f[1] ?? '').match(/Default:\s*`?"?(\w+)/)?.[1] ?? null,
630
+ values,
631
+ // The entity's top-level fields, to name the ones only an "all fields" set returns.
632
+ entityFields: membersOf(entityBody ?? '').map((x) => x.name.replace(/^_/, '')),
633
+ };
634
+ break;
635
+ }
636
+ }
637
+
638
+ for (const meth of methods.values()) {
639
+ if (!['search', 'query', 'query-builder', 'list'].includes(meth.kind)) continue;
640
+ const skip = new Set([entityType]);
641
+ // The first parameter with an object type (`contactId: string` has no keys).
642
+ for (const p of splitParams(rawParams.get(meth.name) ?? '')) {
643
+ const m = p.match(/^(\w+)\??\s*:\s*(?:NonNullablePaths<\s*)?([A-Z]\w*)/);
644
+ const shape = m && typeShape(sources, m[2], skip);
645
+ if (shape) {
646
+ meth.arg = `${m[1]}: ${shape}`;
647
+ break;
648
+ }
649
+ }
650
+ const retType = meth.returns.match(/Promise<(\w+)/)?.[1];
651
+ if (retType) meth.result = typeShape(sources, retType, skip);
652
+ }
653
+
654
+ return {
655
+ package: name,
656
+ version: pj.version,
657
+ fqdn: dep.fqdn ?? null,
658
+ fqdnNamespace: dep.fqdnNamespace ?? null,
659
+ maturity: methods.size && preview === methods.size ? 'PREVIEW' : 'GA',
660
+ methods: [...methods.values()],
661
+ events,
662
+ type: typeAt,
663
+ };
664
+ }
665
+
666
+ // interface XSearchSpec extends SearchSpec {
667
+ // searchable: ['a', 'b'];
668
+ // wql: [{ operators: ['$eq', …]; fields: ['c', …]; sort: 'BOTH' | 'ASC' | 'DESC' | 'NONE' }, …];
669
+ // } → { field: { ops: [...], sort: bool, search: bool } }
670
+ function parseSpec(src, name) {
671
+ const m = new RegExp(`interface ${name} extends \\w+Spec\\s*\\{([\\s\\S]*?)\\n\\}`).exec(src);
672
+ if (!m) return null;
673
+ const body = m[1];
674
+ const list = (s) => [...(s ?? '').matchAll(/'([^']+)'/g)].map((x) => x[1]);
675
+ const out = {};
676
+ const field = (f) => (out[f] ??= { ops: [], sort: false, search: false });
677
+ for (const f of list(body.match(/searchable:\s*\[([\s\S]*?)\]/)?.[1])) field(f).search = true;
678
+ const wql = body.slice(body.indexOf('wql:'));
679
+ for (const block of wql.matchAll(/\{([^{}]*)\}/g)) {
680
+ const ops = list(block[1].match(/operators:\s*(\[[^\]]*\]|'[^']*')/)?.[1]);
681
+ const sort = block[1].match(/sort:\s*'(\w+)'/)?.[1] ?? 'NONE';
682
+ for (const f of list(block[1].match(/fields:\s*\[([^\]]*)\]/)?.[1])) {
683
+ const e = field(f);
684
+ e.ops = [...new Set([...e.ops, ...ops])];
685
+ e.sort = e.sort || sort !== 'NONE';
686
+ }
687
+ }
688
+ // `{}` when the spec is declared but lists no field (`wql: []`): the
689
+ // filter fields are on the method's docs page only.
690
+ return out;
691
+ }
692
+
693
+ function httpOf(metaFile) {
694
+ if (!fs.existsSync(metaFile)) return {};
695
+ const src = fs.readFileSync(metaFile, 'utf8');
696
+ const out = {};
697
+ const re = /function (\w+?)2\(\) \{[\s\S]*?httpMethod: "(\w+)",\s*path: "([^"]+)"/g;
698
+ let m;
699
+ while ((m = re.exec(src))) out[m[1]] = `${m[2]} ${m[3]}`;
700
+ return out;
701
+ }
702
+
703
+
704
+ // --- matching and ranking ----------------------------------------------------
705
+
706
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
707
+ function variants(term) {
708
+ const t = norm(term);
709
+ const out = new Set([t, `${t}s`]);
710
+ if (t.endsWith('ies')) out.add(`${t.slice(0, -3)}y`);
711
+ if (t.endsWith('s')) out.add(t.slice(0, -1));
712
+ return out;
713
+ }
714
+ const versionOf = (fqdn) => Number((fqdn ?? '').match(/\.v(\d+)\./)?.[1] ?? 0);
715
+ const baseOf = (ns) => norm(ns.replace(/V\d+$/, ''));
716
+
717
+ function namespacesOf(name, entry) {
718
+ if (entry.exportedAs.length) return entry.exportedAs;
719
+ return [{ from: name, namespace: null }];
720
+ }
721
+
722
+ function loadCurated() {
723
+ const file = path.join(__dirname, 'sdk-curated.json');
724
+ return fs.existsSync(file) ? readJson(file).rules : [];
725
+ }
726
+
727
+ function moduleMapRow(term) {
728
+ const file = path.join(__dirname, '..', 'references', 'SDK_MODULE_MAP.md');
729
+ if (!fs.existsSync(file)) return null;
730
+ const v = variants(term);
731
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
732
+ const m = line.match(/^\|\s*([^|]+?)\s*\|\s*`(@wix\/[\w-]+)`/);
733
+ if (!m) continue;
734
+ const words = m[1].split(/[\s/,]+/).map(norm);
735
+ if (words.some((w) => v.has(w))) return { entities: m[1], package: m[2] };
736
+ }
737
+ return null;
738
+ }
739
+
740
+ function lookup(term, intent, pkgs, curated) {
741
+ const v = variants(term);
742
+ const primary = [];
743
+ const related = [];
744
+ for (const [name, entry] of pkgs.autos) {
745
+ const pj = readJson(path.join(entry.dir, 'package.json'));
746
+ const dep = pj.wix?.sdkDependency ?? {};
747
+ const last = norm((dep.fqdn ?? '').split('.').pop() ?? '');
748
+ for (const ns of namespacesOf(name, entry)) {
749
+ const nsBase = ns.namespace ? baseOf(ns.namespace) : norm(name.replace(/^@wix\/auto_sdk_[^_]+_/, ''));
750
+ const hay = norm(`${name} ${dep.fqdn ?? ''} ${ns.namespace ?? ''}`);
751
+ if (v.has(nsBase) || v.has(last)) primary.push({ name, entry, ns });
752
+ else if ([...v].some((x) => x.length > 3 && hay.includes(x))) related.push({ name, entry, ns });
753
+ }
754
+ }
755
+
756
+ const rules = curated.filter((r) => r.entities.some((e) => v.has(norm(e))));
757
+ // A curated preference can name a namespace the term itself does not match
758
+ // (a refunds table reads orders), so pull that namespace in.
759
+ const prefer = rules.find((r) => r.choice?.prefer && (!r.choice.intents || r.choice.intents.includes(intent)));
760
+ const siteRule = rules.find((r) => r.choice === 'site');
761
+ const pullIn = [prefer?.choice.prefer, ...(siteRule?.namespaces ?? [])].filter(Boolean);
762
+ for (const want of pullIn) {
763
+ if (primary.some((c) => c.ns.namespace === want)) continue;
764
+ for (const [name, entry] of pkgs.autos) {
765
+ const ns = entry.exportedAs.find((x) => x.namespace === want);
766
+ if (ns) primary.push({ name, entry, ns });
767
+ }
768
+ }
769
+ if (!primary.length) {
770
+ const row = moduleMapRow(term);
771
+ if (row && !pkgs.wrappers.has(row.package)) return { term, status: 'not-installed', row, rules };
772
+ return { term, status: 'unknown', near: nearMatches(term, pkgs), related: related.map(label), rules };
773
+ }
774
+
775
+ const candidates = primary.map((c) => ({ ...c, record: deriveRecord(c.name, c.entry.dir) }));
776
+ const reasons = new Map();
777
+ const why = (c, r) => reasons.set(c, [...(reasons.get(c) ?? []), r]);
778
+ for (const c of candidates) {
779
+ why(c, `fqdn v${versionOf(c.record.fqdn)} (${c.record.fqdn})`);
780
+ if (c.record.maturity === 'PREVIEW') why(c, 'preview');
781
+ if (c.record.methods.length && c.record.methods.every((m) => m.deprecated)) why(c, 'deprecated');
782
+ if (searching(intent)) why(c, c.record.methods.some((m) => m.kind === 'search') ? 'has search' : 'no search method');
783
+ if (!c.ns.namespace) why(c, 'not exported from a public package root (SPI or internal)');
784
+ if (c.ns.installed === false) why(c, `${c.ns.from} not installed`);
785
+ }
786
+
787
+ const preferRule = prefer && candidates.some((c) => c.ns.namespace === prefer.choice.prefer) ? prefer : null;
788
+ // Several packages can export the same namespace (orders: ecom, pricing-plans,
789
+ // events). Across packages the module map's package wins; with no map row,
790
+ // the package named after the namespace (@wix/categories, not @wix/blog),
791
+ // then a package whose entity has a description; then the package name, so
792
+ // the order never depends on which package is read first. The API
793
+ // version and a search method rank only the namespaces of one package
794
+ // (contacts vs contactsV5): across packages they say nothing about the feature.
795
+ const packages = new Set(candidates.filter((c) => c.ns.namespace).map((c) => c.ns.from));
796
+ const mapPackage = packages.size > 1 ? moduleMapRow(term)?.package : null;
797
+ const namesake = (c) =>
798
+ packages.size > 1 && !!c.ns.namespace && norm(c.ns.from.replace(/^@wix\//, '')) === baseOf(c.ns.namespace);
799
+ // Per package, not per namespace, so it never reorders one package's namespaces.
800
+ const described = new Set(candidates.filter((c) => c.record.type?.description).map((c) => c.ns.from));
801
+ const score = (c) => [
802
+ c.record.maturity === 'PREVIEW' || reasons.get(c).includes('deprecated') ? 0 : 1,
803
+ preferRule && c.ns.namespace === preferRule.choice.prefer ? 1 : 0,
804
+ c.ns.namespace ? 1 : 0,
805
+ mapPackage && c.ns.from === mapPackage ? 1 : 0,
806
+ namesake(c) ? 1 : 0,
807
+ packages.size > 1 && described.has(c.ns.from) ? 1 : 0,
808
+ ];
809
+ const withinPackage = (c) => [
810
+ searching(intent) && c.record.methods.some((m) => m.kind === 'search') ? 1 : 0,
811
+ versionOf(c.record.fqdn),
812
+ ];
813
+ const higher = (x, y) => x.findIndex((v, i) => v !== y[i]);
814
+ candidates.sort((a, b) => {
815
+ let i = higher(score(a), score(b));
816
+ if (i >= 0) return score(b)[i] - score(a)[i];
817
+ if (a.ns.from !== b.ns.from) return (a.ns.from ?? '').localeCompare(b.ns.from ?? '');
818
+ i = higher(withinPackage(a), withinPackage(b));
819
+ if (i >= 0) return withinPackage(b)[i] - withinPackage(a)[i];
820
+ return (a.ns.namespace ?? '').localeCompare(b.ns.namespace ?? '');
821
+ });
822
+ if (preferRule) why(candidates[0], 'curated');
823
+ else if (mapPackage && candidates[0].ns.from === mapPackage) why(candidates[0], 'the module map package');
824
+ else if (namesake(candidates[0])) why(candidates[0], 'the package named after it');
825
+ if (packages.size > 1) {
826
+ for (const c of candidates.filter((c) => c.ns.namespace)) c.description = c.record.type?.description ?? null;
827
+ }
828
+ return {
829
+ term,
830
+ status: 'found',
831
+ siteDependent: !!siteRule,
832
+ candidates: candidates.map((c) => ({
833
+ ...c,
834
+ reasons: reasons.get(c),
835
+ site: !!siteRule && siteRule.namespaces.includes(c.ns.namespace),
836
+ })),
837
+ related: related.filter((r) => !primary.some((p) => p.name === r.name)).map(label),
838
+ rules,
839
+ };
840
+ }
841
+
842
+ const label = (c) => (c.ns.namespace ? `${c.ns.namespace} (${c.ns.from})` : c.name);
843
+
844
+ // The call-shape rule: which one call to write for the intent, and why.
845
+ // search: a search method with free text → one call, the term in
846
+ // search.search.expression. Otherwise one filter object with $or,
847
+ // one clause per field. A query builder chains clauses with AND and
848
+ // has no or(): when the method also takes a query object, its
849
+ // filter has $or; else one field with startsWith, never one query
850
+ // per field merged in the client.
851
+ // read: the builder for filters, sort and paging, on the fields its
852
+ // operators list (a closed list), else the filter object. A
853
+ // builder with no filter or sort (extendedBookings) → the query
854
+ // object.
855
+ // write: a method that requires revision → read the entity first.
856
+ function shapeFor(record, ns, intent) {
857
+ // `all` holds everything `search` and `write` hold: an agent that picks it for a
858
+ // search table otherwise gets no shape line and re-derives the call from the typings.
859
+ if (intent === 'all') return [...shapeFor(record, ns, 'search'), ...shapeFor(record, ns, 'write')];
860
+ // Not the trash-bin reads (queryDeletedForms): a search is over live items.
861
+ const by = (k) => record.methods.filter((m) => m.kind === k && !m.deprecated && !/Deleted|TrashBin/.test(m.name));
862
+ const [search] = by('search');
863
+ const [builder] = by('query-builder');
864
+ const [query] = by('query').filter((m) => m.name.toLowerCase().includes(baseOf(ns ?? '')));
865
+ const call = (m) => `${ns ?? 'sdk'}.${m.name}`;
866
+ const textFields = (m) =>
867
+ m?.filterable ? Object.entries(m.filterable).filter(([, f]) => f.search).map(([k]) => k) : null;
868
+ const queryCall = (m) => `${call(m)}({ ${(m.queryForm.keys?.length ? m.queryForm.keys : ['filter', 'sort']).join(', ')} })`;
869
+ const out = [];
870
+ let opt;
871
+
872
+ if (intent === 'search') {
873
+ if (search && search.freeText !== false) {
874
+ const fields = textFields(search);
875
+ out.push(`${call(search)}({ search: { expression: term } }): one call${fields ? `; the term matches ${fields.join(', ')}` : ''}; exact-match facets go in filter`);
876
+ } else if ((opt = [builder, query, ...by('list')].find((m) => m?.optionsSearch))) {
877
+ // Free text in a query's options, with no search method (queryMembers).
878
+ const o = opt.optionsSearch;
879
+ const first = splitParams(opt.params.join(', '))[0]?.startsWith(o.param);
880
+ const arg = `{ search: { expression: term, fields } }`;
881
+ out.push(`${call(opt)}(${first ? arg : `…, ${arg}`})${opt.kind === 'query-builder' ? '.find()' : ''}: no search method; free text in its ${o.param}.search`);
882
+ if (o.default) out.push(` search.fields defaults to ${o.default} alone: name every field the term must match${o.max ? ` (at most ${o.max})` : ''}`);
883
+ if (o.fields.length) out.push(` searchable fields: ${o.fields.join(', ')}`);
884
+ out.push(' never run one query per field and merge the results in the client');
885
+ } else {
886
+ // Without free text, a term goes in $startsWith on the text fields: a
887
+ // prefix match. None of them → the server cannot match a term at all.
888
+ const orBuilder = builder?.builder?.or;
889
+ const form = search ?? query ?? (builder?.queryForm && !orBuilder ? builder : null);
890
+ const target = form ?? builder;
891
+ const listed = target?.textFields;
892
+ const any = target?.textFieldsAny ?? [];
893
+ const fields = listed && [...listed, ...any];
894
+ const where = form === builder ? queryCall(builder) : target && `${call(target)}${target === builder ? '()' : ''}`;
895
+ const merge = ' never run one query per field and merge the results in the client';
896
+ if (fields && !fields.length) {
897
+ // A search argument can carry `search: { expression }` (the generic
898
+ // search object) while the spec declares no searchable field: the
899
+ // type accepts a term the service has no field to match it on.
900
+ const typed = search && /\bsearch: \{[^}]*\bexpression\b/.test(search.arg ?? '');
901
+ out.push(`${where}: no text match in the package: no searchable field, and no text field takes $startsWith`);
902
+ if (typed) out.push(' its argument types search.expression, but its search spec declares no searchable field: do not rely on it unless the docs page lists searchable fields');
903
+ out.push(` next: its docs page (SearchWixSDKDocumentation "${target.name}"), not the typings: nothing more is in the package`);
904
+ out.push(' if the docs list no text filter either, tell the user the API cannot match a search term;');
905
+ out.push(' a search then reads every page (cursor) and matches the values itself, re-read on each search, with no silent cap');
906
+ out.push(' never match only the loaded page, or a capped or cached set, and present it as a search');
907
+ } else if (form) {
908
+ const why = form === builder ? 'no search method, and the builder has no or(): pass one query object instead' : search ? 'its search takes no free text' : 'no search method';
909
+ out.push(`${where}: ${why}`);
910
+ if (form === search) out.push(' a search.expression here can be ignored with no error (on a real site, searchOrders returned every order): put the term in filter');
911
+ const or = fields && `filter: { $or: [${fields.slice(0, 2).map((f) => `{ '${f}': { $startsWith: term } }`).join(', ')}${fields.length > 2 ? ', …' : ''}] }`;
912
+ // The package has nothing more to find when it lists no fields: say so,
913
+ // or the agent greps the typings and the compiled JS for operators.
914
+ if (!fields) {
915
+ out.push(' the term: filter $or, one clause per text field — the package has no filter list for this method');
916
+ out.push(` next: its docs page (SearchWixSDKDocumentation "${target.name} filter"), not the typings or the compiled JS: nothing more is in the package`);
917
+ out.push(' the docs page lists the fields and operators: $contains where a field takes it (a match inside a value), else $startsWith (a prefix)');
918
+ } else {
919
+ out.push(listed.length ? ` the term: ${or} on the text fields ${capped(listed)}` : ` the term: ${or}`);
920
+ if (any.length) {
921
+ out.push(` ${listed.length ? 'also ' : ''}text fields with operators '*' (every operator, $startsWith too): ${capped(any)}`);
922
+ }
923
+ out.push(' $startsWith is a prefix match; the package lists $contains for no field: for a match inside a value, check the method docs page');
924
+ }
925
+ if (form === builder) out.push(' an API may implement only part of WQL: if its docs page rules out $or, use ONE field and say so to the user');
926
+ out.push(merge);
927
+ } else if (orBuilder) {
928
+ out.push(`${where}: no search method; the builder has or(), so chain one .startsWith() per field inside .or()${fields ? ` (${capped(fields)})` : ''}`);
929
+ } else if (builder) {
930
+ out.push(`${where}: no search method, and the builder chains clauses with AND and has no or()`);
931
+ out.push(` so one builder call cannot match a term in any of several fields: use startsWith on ONE field${fields?.length ? ` (${capped(fields)})` : ''}, and say so to the user`);
932
+ out.push(merge);
933
+ }
934
+ }
935
+ } else if (intent === 'read') {
936
+ if (builder) {
937
+ const ops = Object.entries(builder.builder ?? {}).filter(([k, f]) => f.length && !['limit', 'skip', 'skipTo', 'find'].includes(k));
938
+ if (!ops.length && builder.queryForm) {
939
+ out.push(`${queryCall(builder)}: the builder has no filter or sort, so pass one query object`);
940
+ out.push(` fields and operators: ${builder.filterable ? 'its filter list' : 'its docs page (the package does not list them)'}`);
941
+ } else {
942
+ out.push(`${call(builder)}(): filters, sort and paging chained on the builder (AND only); fields per operator are a closed list:`);
943
+ for (const [op, f] of ops) out.push(` .${op}(${f.includes('*') ? 'any field' : capped(f, 10)})`);
944
+ if (builder.queryForm) out.push(` or one query object, ${queryCall(builder)}, whose filter also takes $or`);
945
+ }
946
+ if (search) out.push(` a search box over several fields → ${call(search)} instead (--intent search)`);
947
+ } else if (search || query) {
948
+ out.push(`${call(search ?? query)}: one filter object (fields from its filter list or its docs page)`);
949
+ }
950
+ } else if (intent === 'write') {
951
+ const needsRevision = record.methods.filter((m) => /^(update|delete)/.test(m.kind) && m.required.some((r) => /revision$/.test(r)));
952
+ if (needsRevision.length) {
953
+ out.push(`${needsRevision.map(call).join(', ')} require revision: read the entity first and pass its current revision (a stale one fails)`);
954
+ }
955
+ }
956
+ return out;
957
+ }
958
+
959
+ function nearMatches(term, pkgs) {
960
+ const t = norm(term);
961
+ const names = new Set();
962
+ for (const [name, entry] of pkgs.autos) for (const ns of namespacesOf(name, entry)) names.add(ns.namespace ?? name);
963
+ const dist = (a, b) => {
964
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
965
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
966
+ for (let i = 1; i <= a.length; i++)
967
+ for (let j = 1; j <= b.length; j++)
968
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
969
+ return d[a.length][b.length];
970
+ };
971
+ return [...names]
972
+ .map((n) => [n, norm(n).includes(t) || t.includes(baseOf(n)) ? 0 : dist(t, baseOf(n))])
973
+ .filter(([, d]) => d <= 3)
974
+ .sort((a, b) => a[1] - b[1])
975
+ .slice(0, 5)
976
+ .map(([n]) => n);
977
+ }
978
+
979
+ // --- output ------------------------------------------------------------------
980
+
981
+ // filterable: { field: { ops: [...], sort: bool, search: bool } } — a closed
982
+ // list per method, so a field that is not here cannot be filtered on.
983
+ // The smallest set for each field, computed, not left to the agent: a run that
984
+ // read "EXTENDED: … and the `profile` object" chose EXTENDED for a name column,
985
+ // and EXTENDED has no `contact`. Sets the package does not describe by field
986
+ // (forms: NESTED_FORMS) print their own text.
987
+ function renderFieldsets(m) {
988
+ const { default: dflt, values, entityFields } = m.fieldsets;
989
+ if (!values.some((v) => Array.isArray(v.includes) && v.includes.length)) {
990
+ return values.map((v) => `${v.name}: ${v.fields}`).join(' · ');
991
+ }
992
+ const seen = new Set();
993
+ const parts = values.map((v) => {
994
+ if (v.includes === 'all') {
995
+ const only = entityFields.filter((f) => !seen.has(f));
996
+ return `${v.name}: every field${only.length ? ` — only here: ${only.join(', ')}` : ''}`;
997
+ }
998
+ const added = v.includes.filter((f) => !seen.has(f));
999
+ added.forEach((f) => seen.add(f));
1000
+ return `${v.name}${v === values[0] ? '' : ' adds'}: ${added.join(', ')}`;
1001
+ });
1002
+ return `${dflt ? `default ${dflt}; ` : ''}ask for the smallest set that returns every field you show — ${parts.join(' · ')}`;
1003
+ }
1004
+
1005
+ function renderFilterable(filterable) {
1006
+ const rows = Object.entries(filterable);
1007
+ const text = rows.filter(([, f]) => f.search).map(([k]) => k);
1008
+ const sort = rows.filter(([, f]) => f.sort).map(([k]) => k);
1009
+ const byOps = new Map();
1010
+ for (const [k, f] of rows) {
1011
+ const ops = (f.ops ?? []).join(' ');
1012
+ // A free-text-only field (contacts' `name.full`) has no operators: the free-text line covers it.
1013
+ if (!ops) continue;
1014
+ byOps.set(ops, [...(byOps.get(ops) ?? []), k]);
1015
+ }
1016
+ const out = [];
1017
+ if (text.length) out.push(`free text: ${text.join(', ')}`);
1018
+ for (const [ops, fields] of byOps) out.push(`filter ${ops}: ${capped(fields, 12)}`);
1019
+ if (sort.length) out.push(`sort: ${capped(sort, 12)}`);
1020
+ out.push('(closed list: a field not shown cannot be filtered or sorted on)');
1021
+ return out;
1022
+ }
1023
+
1024
+ const capped = (list, n = 8) => (list.length > n ? `${list.slice(0, n).join(', ')}, +${list.length - n} more` : list.join(', '));
1025
+
1026
+ function render(result, intent, project) {
1027
+ const out = [`== ${result.term} (intent: ${intent})`];
1028
+ const rel = (f) => path.relative(project, f) || f;
1029
+ if (result.status === 'not-installed') {
1030
+ out.push(`not installed: "${result.row.entities}" → ${result.row.package}`);
1031
+ out.push(` npm i ${result.row.package} then run this lookup again`);
1032
+ } else if (result.status === 'unknown') {
1033
+ out.push('no installed SDK namespace matches this name');
1034
+ if (result.near.length) out.push(` near: ${result.near.join(', ')}`);
1035
+ if (result.related.length) out.push(` related: ${capped(result.related)}`);
1036
+ out.push(' if it is a Wix business entity, check references/SDK_MODULE_MAP.md, then search the SDK docs');
1037
+ } else {
1038
+ // A site-dependent entity has one namespace per site kind, all shown in full.
1039
+ const chosen = result.siteDependent ? result.candidates.filter((c) => c.site) : result.candidates.slice(0, 1);
1040
+ const lead = result.siteDependent ? 'site' : 'use';
1041
+ // Packages that export the same namespace: each one's entity description,
1042
+ // so the agent switches when the feature matches another package.
1043
+ const what = (c) =>
1044
+ c.description === undefined ? '' : c.description ? ` — "${c.description}"` : ' — no description in the package';
1045
+ for (const c of chosen) out.push(`${lead.padEnd(5)} ${label(c)} — ${c.reasons.join('; ')}${what(c)}`);
1046
+ for (const c of result.candidates.filter((c) => !chosen.includes(c))) {
1047
+ out.push(`also ${label(c)} — ${c.reasons.join('; ')}${what(c)}`);
1048
+ }
1049
+ if (result.candidates.some((c) => c.description !== undefined)) {
1050
+ out.push(' several packages export this name: if the feature matches another package\'s description, use that one');
1051
+ }
1052
+ for (const c of chosen) out.push(...renderCandidate(c, intent, rel));
1053
+ if (result.related.length) out.push(`related: ${capped(result.related)}`);
1054
+ }
1055
+ for (const r of result.rules) out.push(`note ${r.note} [${r.source}]`);
1056
+ return out.join('\n');
1057
+ }
1058
+
1059
+ function renderCandidate(c, intent, rel) {
1060
+ const r = c.record;
1061
+ const ns = c.ns.namespace;
1062
+ const out = [];
1063
+ if (!ns) out.push(`// ${c.name} is not exported from a public package root; check its wrapper's subpath exports`);
1064
+ else if (c.ns.installed === false) out.push(`// ${c.ns.from} is not installed (only pulled in by another package): npm i ${c.ns.from}`);
1065
+ out.push(ns ? `import { ${ns} } from '${c.ns.from}';` : `import * as sdk from '${c.name}';`);
1066
+ // Before the methods: a long method list must not push the declaration file
1067
+ // past an agent's `| head`.
1068
+ if (r.type) out.push(` type ${r.type.name} ${rel(r.type.file)}:${r.type.line}`);
1069
+ out.push(` package ${r.package}@${r.version}`);
1070
+ const shape = shapeFor(r, ns, intent);
1071
+ if (shape.length) out.push(`shape ${shape[0]}`, ...shape.slice(1).map((l) => ` ${l}`));
1072
+ const kinds = INTENTS[intent];
1073
+ const shown =
1074
+ intent === 'event'
1075
+ ? []
1076
+ : r.methods
1077
+ .filter((m) => !kinds || kinds.includes(m.kind))
1078
+ .sort((a, b) => {
1079
+ const order = kinds ?? ALL_ORDER;
1080
+ const rank = (m) => (order.includes(m.kind) ? order.indexOf(m.kind) : order.length);
1081
+ return rank(a) - rank(b);
1082
+ });
1083
+ for (const m of shown) {
1084
+ const call = `${ns ?? 'sdk'}.${m.name}(${m.params.join(', ')}) → ${m.returns}`;
1085
+ out.push(` ${m.kind.padEnd(13)} ${call}`);
1086
+ // @applicableIdentity is APP on visitor methods too (addLineItemsToCurrentCart,
1087
+ // getCurrentMember), so it says nothing about who may call: not printed. The
1088
+ // JSDoc notes say it when the docs page does.
1089
+ const facts = [`permission ${m.permission ?? '?'}`];
1090
+ facts.push(m.scopes?.length ? `scope ${m.scopes.join(' | ')}` : 'scope: not in the package (the method docs page lists it)');
1091
+ if (m.admin) facts.push('admin method');
1092
+ if (m.required.length) facts.push(`required ${m.required.join(', ')}`);
1093
+ if (m.maturity !== 'GA') facts.push(m.maturity.toLowerCase());
1094
+ if (m.deprecated) facts.push(`DEPRECATED${m.deprecated.replacedBy ? ` → ${m.deprecated.replacedBy}` : ''}`);
1095
+ out.push(` ${''.padEnd(13)} ${facts.join(' · ')}`);
1096
+ for (const n of m.notes ?? []) out.push(` ${''.padEnd(13)} note ${n}`);
1097
+ if (m.arg) out.push(` ${''.padEnd(13)} arg ${m.arg}`);
1098
+ if (m.fieldsets) out.push(` ${''.padEnd(13)} fieldsets ${renderFieldsets(m)}`);
1099
+ if (m.result) out.push(` ${''.padEnd(13)} returns ${m.result}`);
1100
+ if (m.queryForm) {
1101
+ const q = m.queryForm;
1102
+ out.push(` ${''.padEnd(13)} or ${ns ?? 'sdk'}.${m.name}(${q.params.join(', ')}) → ${q.returns}`);
1103
+ if (q.arg) out.push(` ${''.padEnd(13)} arg ${q.arg}`);
1104
+ if (q.result) out.push(` ${''.padEnd(13)} returns ${q.result}`);
1105
+ }
1106
+ if (m.filterable) out.push(...renderFilterable(m.filterable).map((l) => ` ${''.padEnd(13)} ${l}`));
1107
+ else if (m.filterUnlisted) out.push(` ${''.padEnd(13)} filter fields: not in the package (the method docs page lists them)`);
1108
+ // The declaration, for the types behind `arg` and `returns`: the public
1109
+ // call is in index.d.ts, not in the hashed file the entity type is in.
1110
+ const decl = m.decl && `decl ${rel(m.decl.file)}:${m.decl.line}`;
1111
+ if (m.fqn || decl) out.push(` ${''.padEnd(13)} ${[m.fqn && `fqn ${m.fqn}`, decl].filter(Boolean).join(' · ')}`);
1112
+ }
1113
+ const others = r.methods.filter((m) => !shown.includes(m)).map((m) => m.name);
1114
+ if (others.length && intent !== 'all') {
1115
+ const next = intent === 'write' ? '--intent all' : '--intent write (or all)';
1116
+ out.push(` other methods (params, permission, scope: run again with ${next}): ${others.join(', ')}`);
1117
+ }
1118
+ if (intent === 'event' || intent === 'all') {
1119
+ for (const e of r.events) out.push(` event ${ns ?? 'sdk'}.${e.name}(handler) · scope ${e.scopes.join(' | ') || '?'}`);
1120
+ } else if (r.events.length) {
1121
+ out.push(` events: ${r.events.map((e) => e.name).join(', ')}`);
1122
+ }
1123
+ return out;
1124
+ }
1125
+
1126
+ // --- main --------------------------------------------------------------------
1127
+
1128
+ function main(argv) {
1129
+ const args = [...argv];
1130
+ const flag = (n) => {
1131
+ const i = args.indexOf(n);
1132
+ return i >= 0 ? args.splice(i, 2)[1] : undefined;
1133
+ };
1134
+ const intent = flag('--intent') ?? 'read';
1135
+ const project = path.resolve(flag('--project') ?? process.cwd());
1136
+ const asJson = args.includes('--json');
1137
+ const terms = args.filter((a) => !a.startsWith('--'));
1138
+ if (!terms.length || !(intent in INTENTS)) {
1139
+ console.error('usage: sdk-lookup.cjs <entity> [<entity> ...] [--intent search|read|write|event|all] [--json]');
1140
+ return 2;
1141
+ }
1142
+ activatePnp(project);
1143
+ let pkgs;
1144
+ try {
1145
+ pkgs = findPackages(project);
1146
+ } catch (e) {
1147
+ if (!(e instanceof LookupError)) throw e;
1148
+ console.error(`sdk-lookup: ${e.message}`);
1149
+ return 4;
1150
+ }
1151
+ if (!pkgs.autos.size) {
1152
+ console.error(`no @wix/auto_sdk_* packages found from ${project}; install the vertical's @wix/<package> first`);
1153
+ return 3;
1154
+ }
1155
+ const curated = loadCurated();
1156
+ const results = terms.map((t) => lookup(t, intent, pkgs, curated));
1157
+ if (asJson) {
1158
+ const plain = results.map((r) => ({
1159
+ ...r,
1160
+ candidates: r.candidates?.map((c) => ({
1161
+ namespace: c.ns.namespace,
1162
+ from: c.ns.from,
1163
+ reasons: c.reasons,
1164
+ description: c.description,
1165
+ site: c.site,
1166
+ shape: shapeFor(c.record, c.ns.namespace, intent),
1167
+ record: c.record,
1168
+ })),
1169
+ }));
1170
+ console.log(JSON.stringify(plain, null, 2));
1171
+ } else {
1172
+ console.log(results.map((r) => render(r, intent, project)).join('\n\n'));
1173
+ }
1174
+ const codes = results.map((r) => ({ found: 0, unknown: 1, 'not-installed': 3 })[r.status]);
1175
+ return Math.max(...codes);
1176
+ }
1177
+
1178
+ if (require.main === module) process.exitCode = main(process.argv.slice(2));
1179
+
1180
+ module.exports = { findPackages, deriveRecord, lookup, loadCurated };