@soederpop/luca 0.0.25 → 0.0.26
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/src/agi/features/assistant.ts +14 -12
- package/src/agi/features/docs-reader.ts +25 -1
- package/src/bootstrap/generated.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/command.ts +75 -0
- package/src/commands/describe.ts +29 -1089
- package/src/container-describer.ts +1098 -0
- package/src/container.ts +11 -0
- package/src/introspection/generated.agi.ts +513 -429
- package/src/introspection/generated.node.ts +503 -419
- package/src/introspection/generated.web.ts +9 -1
- package/src/node/features/content-db.ts +17 -0
- package/src/node/features/fs.ts +18 -0
- package/src/scaffolds/generated.ts +1 -1
- package/src/server.ts +40 -0
- package/src/servers/express.ts +2 -0
- package/src/servers/mcp.ts +1 -0
- package/src/servers/socket.ts +2 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
import type { IntrospectionSection, MethodIntrospection, GetterIntrospection, HelperIntrospection } from './introspection/index.js'
|
|
2
|
+
import { presentIntrospectionAsMarkdown } from './helper.js'
|
|
3
|
+
|
|
4
|
+
type Platform = 'browser' | 'server' | 'node' | 'all'
|
|
5
|
+
|
|
6
|
+
type ResolvedTarget =
|
|
7
|
+
| { kind: 'container' }
|
|
8
|
+
| { kind: 'registry'; name: string }
|
|
9
|
+
| { kind: 'helper'; registry: string; id: string }
|
|
10
|
+
| { kind: 'member'; registry: string; id: string; member: string; memberType: 'method' | 'getter' }
|
|
11
|
+
| { kind: 'browser-helper'; id: string }
|
|
12
|
+
| { kind: 'browser-member'; id: string; member: string; memberType: 'method' | 'getter' }
|
|
13
|
+
|
|
14
|
+
type DescribeOptions = {
|
|
15
|
+
sections?: (IntrospectionSection | 'description')[]
|
|
16
|
+
noTitle?: boolean
|
|
17
|
+
headingDepth?: number
|
|
18
|
+
platform?: Platform
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type DescribeResult = { json: any; text: string }
|
|
22
|
+
|
|
23
|
+
type BrowserFeatureData = {
|
|
24
|
+
introspection: Map<string, HelperIntrospection>
|
|
25
|
+
constructors: Map<string, any>
|
|
26
|
+
available: string[]
|
|
27
|
+
collidingIds: Set<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class DescribeError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message)
|
|
33
|
+
this.name = 'DescribeError'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Known top-level helper base class names — anything above these is "shared" */
|
|
38
|
+
const BASE_CLASS_NAMES = new Set(['Helper', 'Feature', 'Client', 'Server'])
|
|
39
|
+
|
|
40
|
+
/** Maps flag names to the section they represent. */
|
|
41
|
+
const SECTION_FLAGS: Record<string, IntrospectionSection | 'description'> = {
|
|
42
|
+
'description': 'description',
|
|
43
|
+
'usage': 'usage',
|
|
44
|
+
'methods': 'methods',
|
|
45
|
+
'getters': 'getters',
|
|
46
|
+
'events': 'events',
|
|
47
|
+
'state': 'state',
|
|
48
|
+
'options': 'options',
|
|
49
|
+
'env-vars': 'envVars',
|
|
50
|
+
'envvars': 'envVars',
|
|
51
|
+
'examples': 'examples',
|
|
52
|
+
'only-methods': 'methods',
|
|
53
|
+
'only-getters': 'getters',
|
|
54
|
+
'only-events': 'events',
|
|
55
|
+
'only-state': 'state',
|
|
56
|
+
'only-options': 'options',
|
|
57
|
+
'only-env-vars': 'envVars',
|
|
58
|
+
'only-envvars': 'envVars',
|
|
59
|
+
'only-examples': 'examples',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Encapsulates container introspection and description logic.
|
|
64
|
+
* Discovers registries dynamically from the container's own state —
|
|
65
|
+
* it knows nothing about which helpers exist until it asks the container.
|
|
66
|
+
* Browser feature data can be injected externally via setBrowserData().
|
|
67
|
+
*/
|
|
68
|
+
export class ContainerDescriber {
|
|
69
|
+
container: any
|
|
70
|
+
private _browserData: BrowserFeatureData | null = null
|
|
71
|
+
private _initialized = false
|
|
72
|
+
|
|
73
|
+
constructor(container: any) {
|
|
74
|
+
this.container = container
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The registry names this container actually has, discovered at runtime. */
|
|
78
|
+
private get registryNames(): string[] {
|
|
79
|
+
return this.container.registryNames || ['features']
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Discover all helpers. Must be called before resolve/getData.
|
|
84
|
+
*/
|
|
85
|
+
async initialize(): Promise<void> {
|
|
86
|
+
if (this._initialized) return
|
|
87
|
+
await this.container.helpers.discoverAll()
|
|
88
|
+
this._initialized = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Inject browser feature data from an external source.
|
|
93
|
+
* The describer doesn't own browser loading — that's the caller's job.
|
|
94
|
+
*/
|
|
95
|
+
setBrowserData(data: BrowserFeatureData): void {
|
|
96
|
+
this._browserData = data
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* High-level: describe one or more targets, returning combined json and text.
|
|
101
|
+
*/
|
|
102
|
+
async describe(targets: string[], options: DescribeOptions = {}): Promise<DescribeResult> {
|
|
103
|
+
const platform = this.normalizePlatform(options.platform || 'all')
|
|
104
|
+
await this.initialize()
|
|
105
|
+
|
|
106
|
+
const sections = options.sections || []
|
|
107
|
+
const noTitle = options.noTitle || false
|
|
108
|
+
|
|
109
|
+
const resolved: ResolvedTarget[] = []
|
|
110
|
+
for (const target of targets) {
|
|
111
|
+
resolved.push(...this.resolve(target, platform))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isMulti = resolved.length > 1 || resolved.some((r) => r.kind === 'registry')
|
|
115
|
+
const headingDepth = options.headingDepth ?? (isMulti ? 2 : 1)
|
|
116
|
+
|
|
117
|
+
const results = resolved.map((item) => this.getData(item, { sections, noTitle, headingDepth, platform }))
|
|
118
|
+
|
|
119
|
+
if (resolved.length === 1) {
|
|
120
|
+
return results[0]!
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
json: results.map((r) => r.json),
|
|
125
|
+
text: `# Luca Helper Descriptions\n\nBelow you'll find documentation.\n\n${results.map((r) => r.text).join('\n\n---\n\n')}`,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Describe the container itself.
|
|
131
|
+
*/
|
|
132
|
+
async describeContainer(options: DescribeOptions = {}): Promise<DescribeResult> {
|
|
133
|
+
await this.initialize()
|
|
134
|
+
return this.getContainerData(options.sections || [], options.noTitle || false, options.headingDepth || 1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Describe a registry by name.
|
|
139
|
+
*/
|
|
140
|
+
async describeRegistry(registryName: string, options: DescribeOptions = {}): Promise<DescribeResult> {
|
|
141
|
+
const platform = this.normalizePlatform(options.platform || 'all')
|
|
142
|
+
await this.initialize()
|
|
143
|
+
const name = this.matchRegistryName(registryName)
|
|
144
|
+
if (!name) throw new DescribeError(`Unknown registry: ${registryName}. Available: ${this.registryNames.join(', ')}`)
|
|
145
|
+
return this.getRegistryData(name, options.sections || [], options.noTitle || false, options.headingDepth || 1, platform)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Describe a specific helper by name (qualified or unqualified).
|
|
150
|
+
*/
|
|
151
|
+
async describeHelper(target: string, options: DescribeOptions = {}): Promise<DescribeResult> {
|
|
152
|
+
const platform = this.normalizePlatform(options.platform || 'all')
|
|
153
|
+
await this.initialize()
|
|
154
|
+
|
|
155
|
+
const resolved = this.resolve(target, platform)
|
|
156
|
+
if (resolved.length === 0) throw new DescribeError(`Could not resolve: ${target}`)
|
|
157
|
+
|
|
158
|
+
const headingDepth = options.headingDepth ?? (resolved.length > 1 ? 2 : 1)
|
|
159
|
+
const results = resolved.map((item) => this.getData(item, { ...options, headingDepth, platform }))
|
|
160
|
+
|
|
161
|
+
if (results.length === 1) return results[0]!
|
|
162
|
+
return {
|
|
163
|
+
json: results.map((r) => r.json),
|
|
164
|
+
text: results.map((r) => r.text).join('\n\n---\n\n'),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Describe a specific member (method or getter) on a helper.
|
|
170
|
+
*/
|
|
171
|
+
async describeMember(helperAndMember: string, options: DescribeOptions = {}): Promise<DescribeResult> {
|
|
172
|
+
return this.describeHelper(helperAndMember, options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Collect sections from a flags object (as produced by the CLI args schema).
|
|
177
|
+
*/
|
|
178
|
+
static getSectionsFromFlags(flags: Record<string, any>): (IntrospectionSection | 'description')[] {
|
|
179
|
+
const sections: (IntrospectionSection | 'description')[] = []
|
|
180
|
+
for (const [flag, section] of Object.entries(SECTION_FLAGS)) {
|
|
181
|
+
if (flags[flag] && !sections.includes(section)) {
|
|
182
|
+
sections.push(section)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return sections
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate tool definitions suitable for use with AI assistant tool-calling interfaces.
|
|
190
|
+
* Registry names in the enum are populated dynamically from the container.
|
|
191
|
+
*/
|
|
192
|
+
toTools(): Array<{ name: string; description: string; parameters: Record<string, any>; execute: (args: any) => Promise<string> }> {
|
|
193
|
+
const registryEnum = this.registryNames
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
name: 'describe_container',
|
|
198
|
+
description: 'Describe the container itself — its class, registries, and configuration.',
|
|
199
|
+
parameters: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
sections: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
|
|
205
|
+
description: 'Which sections to include. Omit for all.',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: [],
|
|
209
|
+
},
|
|
210
|
+
execute: async (args: any) => {
|
|
211
|
+
const result = await this.describeContainer({ sections: args.sections })
|
|
212
|
+
return result.text
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'describe_registry',
|
|
217
|
+
description: `List all helpers in a registry with concise summaries. Available registries: ${registryEnum.join(', ')}`,
|
|
218
|
+
parameters: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
registry: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
enum: registryEnum,
|
|
224
|
+
description: 'Which registry to describe.',
|
|
225
|
+
},
|
|
226
|
+
platform: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
enum: ['browser', 'server', 'all'],
|
|
229
|
+
description: 'Filter by platform. Defaults to all.',
|
|
230
|
+
},
|
|
231
|
+
sections: {
|
|
232
|
+
type: 'array',
|
|
233
|
+
items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
|
|
234
|
+
description: 'Which sections to include per helper. Omit for concise index.',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
required: ['registry'],
|
|
238
|
+
},
|
|
239
|
+
execute: async (args: any) => {
|
|
240
|
+
const result = await this.describeRegistry(args.registry, {
|
|
241
|
+
sections: args.sections,
|
|
242
|
+
platform: args.platform,
|
|
243
|
+
})
|
|
244
|
+
return result.text
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'describe_helper',
|
|
249
|
+
description: 'Describe a specific helper by name. Supports qualified names like "features.fs" or unqualified like "fs". Also supports member access like "fs.readFile" or "ui.banner".',
|
|
250
|
+
parameters: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
target: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'The helper to describe. Examples: "fs", "features.fs", "fs.readFile", "ui.banner"',
|
|
256
|
+
},
|
|
257
|
+
platform: {
|
|
258
|
+
type: 'string',
|
|
259
|
+
enum: ['browser', 'server', 'all'],
|
|
260
|
+
description: 'Filter by platform. Defaults to all.',
|
|
261
|
+
},
|
|
262
|
+
sections: {
|
|
263
|
+
type: 'array',
|
|
264
|
+
items: { type: 'string', enum: ['description', 'usage', 'methods', 'getters', 'events', 'state', 'options', 'envVars', 'examples'] },
|
|
265
|
+
description: 'Which sections to include. Omit for all.',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
required: ['target'],
|
|
269
|
+
},
|
|
270
|
+
execute: async (args: any) => {
|
|
271
|
+
const result = await this.describeHelper(args.target, {
|
|
272
|
+
sections: args.sections,
|
|
273
|
+
platform: args.platform,
|
|
274
|
+
})
|
|
275
|
+
return result.text
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Resolution ---
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Parse a target string into one or more resolved targets.
|
|
285
|
+
*/
|
|
286
|
+
resolve(target: string, platform: Platform = 'all'): ResolvedTarget[] {
|
|
287
|
+
const lower = target.toLowerCase()
|
|
288
|
+
const includeNode = this.shouldIncludeNode(platform)
|
|
289
|
+
const includeBrowser = this.shouldIncludeBrowser(platform)
|
|
290
|
+
|
|
291
|
+
if (lower === 'container' || lower === 'self') {
|
|
292
|
+
return [{ kind: 'container' }]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check if it matches a registry name
|
|
296
|
+
const registryMatch = this.matchRegistryName(target)
|
|
297
|
+
if (registryMatch && !target.includes('.')) {
|
|
298
|
+
return [{ kind: 'registry', name: registryMatch }]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (target.includes('.')) {
|
|
302
|
+
const [prefix, ...rest] = target.split('.')
|
|
303
|
+
const id = rest.join('.')
|
|
304
|
+
const registry = this.matchRegistryName(prefix!)
|
|
305
|
+
|
|
306
|
+
if (registry) {
|
|
307
|
+
const results: ResolvedTarget[] = []
|
|
308
|
+
|
|
309
|
+
if (includeNode) {
|
|
310
|
+
const reg = this.container[registry]
|
|
311
|
+
const resolved = this.fuzzyFind(reg, id)
|
|
312
|
+
if (resolved) results.push({ kind: 'helper', registry, id: resolved })
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (includeBrowser && registry === 'features') {
|
|
316
|
+
const browserFound = this.fuzzyFindBrowser(id)
|
|
317
|
+
if (browserFound) results.push({ kind: 'browser-helper', id: browserFound })
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (results.length === 0) {
|
|
321
|
+
const reg = this.container[registry]
|
|
322
|
+
const availableMsg = includeNode ? reg.available.join(', ') : ''
|
|
323
|
+
const browserMsg = includeBrowser && this._browserData ? this._browserData.available.join(', ') : ''
|
|
324
|
+
const combined = [availableMsg, browserMsg].filter(Boolean).join(', ')
|
|
325
|
+
throw new DescribeError(`"${id}" is not registered in ${registry}. Available: ${combined}`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Not a registry prefix — try "helper.member"
|
|
332
|
+
const helperName = prefix!
|
|
333
|
+
const memberName = rest.join('.')
|
|
334
|
+
const results: ResolvedTarget[] = []
|
|
335
|
+
|
|
336
|
+
if (includeNode) {
|
|
337
|
+
const memberResult = this.resolveHelperMember(helperName, memberName)
|
|
338
|
+
if (memberResult) results.push(memberResult)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (includeBrowser) {
|
|
342
|
+
try {
|
|
343
|
+
const browserResult = this.resolveBrowserHelperMember(helperName, memberName)
|
|
344
|
+
if (browserResult) results.push(browserResult)
|
|
345
|
+
} catch (e) {
|
|
346
|
+
if (results.length === 0) throw e
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (results.length > 0) return results
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Unqualified name: search all registries
|
|
354
|
+
const matches: ResolvedTarget[] = []
|
|
355
|
+
|
|
356
|
+
if (includeNode) {
|
|
357
|
+
for (const registryName of this.registryNames) {
|
|
358
|
+
const reg = this.container[registryName]
|
|
359
|
+
if (!reg) continue
|
|
360
|
+
const found = this.fuzzyFind(reg, target)
|
|
361
|
+
if (found) {
|
|
362
|
+
matches.push({ kind: 'helper', registry: registryName, id: found })
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (includeBrowser) {
|
|
368
|
+
const browserFound = this.fuzzyFindBrowser(target)
|
|
369
|
+
if (browserFound) {
|
|
370
|
+
matches.push({ kind: 'browser-helper', id: browserFound })
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (matches.length === 0) {
|
|
375
|
+
const lines = [`"${target}" was not found in any registry.`, '', 'Available:']
|
|
376
|
+
if (includeNode) {
|
|
377
|
+
for (const registryName of this.registryNames) {
|
|
378
|
+
const reg = this.container[registryName]
|
|
379
|
+
if (reg && reg.available.length > 0) {
|
|
380
|
+
lines.push(` ${registryName}: ${reg.available.join(', ')}`)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (includeBrowser && this._browserData && this._browserData.available.length > 0) {
|
|
385
|
+
lines.push(` browser features: ${this._browserData.available.join(', ')}`)
|
|
386
|
+
}
|
|
387
|
+
throw new DescribeError(lines.join('\n'))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const nodeMatches = matches.filter(m => m.kind === 'helper')
|
|
391
|
+
if (nodeMatches.length > 1) {
|
|
392
|
+
const lines = [`"${target}" is ambiguous — found in multiple registries:`]
|
|
393
|
+
for (const m of nodeMatches) {
|
|
394
|
+
if (m.kind === 'helper') lines.push(` ${m.registry}.${m.id}`)
|
|
395
|
+
}
|
|
396
|
+
lines.push('', `Please qualify it, e.g.: ${(nodeMatches[0] as any).registry}.${target}`)
|
|
397
|
+
throw new DescribeError(lines.join('\n'))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return matches
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get data for a resolved target.
|
|
405
|
+
*/
|
|
406
|
+
getData(item: ResolvedTarget, options: DescribeOptions = {}): DescribeResult {
|
|
407
|
+
const sections = options.sections || []
|
|
408
|
+
const noTitle = options.noTitle || false
|
|
409
|
+
const headingDepth = options.headingDepth || 1
|
|
410
|
+
const platform = options.platform || 'all'
|
|
411
|
+
|
|
412
|
+
switch (item.kind) {
|
|
413
|
+
case 'container':
|
|
414
|
+
return this.getContainerData(sections, noTitle, headingDepth)
|
|
415
|
+
case 'registry':
|
|
416
|
+
return this.getRegistryData(item.name, sections, noTitle, headingDepth, platform)
|
|
417
|
+
case 'helper':
|
|
418
|
+
return this.getHelperData(item.registry, item.id, sections, noTitle, headingDepth)
|
|
419
|
+
case 'member':
|
|
420
|
+
return this.getMemberData(item.registry, item.id, item.member, item.memberType, headingDepth)
|
|
421
|
+
case 'browser-helper':
|
|
422
|
+
return this.getBrowserHelperData(item.id, sections, noTitle, headingDepth)
|
|
423
|
+
case 'browser-member':
|
|
424
|
+
return this.getBrowserMemberData(item.id, item.member, item.memberType, headingDepth)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// --- Private: Platform helpers ---
|
|
429
|
+
|
|
430
|
+
private normalizePlatform(p: string): Platform {
|
|
431
|
+
if (p === 'node') return 'server'
|
|
432
|
+
if (p === 'web') return 'browser'
|
|
433
|
+
return p as Platform
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private shouldIncludeNode(platform: Platform): boolean {
|
|
437
|
+
return platform === 'server' || platform === 'node' || platform === 'all'
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private shouldIncludeBrowser(platform: Platform): boolean {
|
|
441
|
+
return platform === 'browser' || platform === 'all'
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- Private: Fuzzy matching ---
|
|
445
|
+
|
|
446
|
+
private normalize(name: string): string {
|
|
447
|
+
return name.replace(/\.[tj]sx?$/, '').replace(/[-_]/g, '').toLowerCase()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fuzzyFind(registry: any, input: string): string | undefined {
|
|
451
|
+
if (registry.has(input)) return input
|
|
452
|
+
const norm = this.normalize(input)
|
|
453
|
+
return (registry.available as string[]).find((id: string) => this.normalize(id) === norm)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private fuzzyFindBrowser(input: string): string | undefined {
|
|
457
|
+
if (!this._browserData) return undefined
|
|
458
|
+
const norm = this.normalize(input)
|
|
459
|
+
return this._browserData.available.find(id => this.normalize(id) === norm)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Match a user-provided name to an actual registry name on the container.
|
|
464
|
+
* Handles pluralization and case variations dynamically.
|
|
465
|
+
*/
|
|
466
|
+
private matchRegistryName(name: string): string | undefined {
|
|
467
|
+
const lower = name.toLowerCase()
|
|
468
|
+
return this.registryNames.find(
|
|
469
|
+
(r) => r === lower || r === lower + 's' || r.replace(/s$/, '') === lower
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// --- Private: Member resolution ---
|
|
474
|
+
|
|
475
|
+
private resolveHelperMember(helperName: string, memberName: string): ResolvedTarget | null {
|
|
476
|
+
for (const registryName of this.registryNames) {
|
|
477
|
+
const reg = this.container[registryName]
|
|
478
|
+
if (!reg) continue
|
|
479
|
+
const found = this.fuzzyFind(reg, helperName)
|
|
480
|
+
if (!found) continue
|
|
481
|
+
|
|
482
|
+
const Ctor = reg.lookup(found)
|
|
483
|
+
const introspection = Ctor.introspect?.()
|
|
484
|
+
if (!introspection) continue
|
|
485
|
+
|
|
486
|
+
if (introspection.methods?.[memberName]) {
|
|
487
|
+
return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'method' }
|
|
488
|
+
}
|
|
489
|
+
if (introspection.getters?.[memberName]) {
|
|
490
|
+
return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'getter' }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const allMembers = [
|
|
494
|
+
...Object.keys(introspection.methods || {}).map((m: string) => m + '()'),
|
|
495
|
+
...Object.keys(introspection.getters || {}),
|
|
496
|
+
].sort()
|
|
497
|
+
throw new DescribeError(
|
|
498
|
+
`"${memberName}" is not a known method or getter on ${found}.\n\nAvailable members:\n ${allMembers.join(', ')}`
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private resolveBrowserHelperMember(helperName: string, memberName: string): ResolvedTarget | null {
|
|
505
|
+
if (!this._browserData) return null
|
|
506
|
+
const found = this.fuzzyFindBrowser(helperName)
|
|
507
|
+
if (!found) return null
|
|
508
|
+
|
|
509
|
+
const data = this._browserData.introspection.get(`features.${found}`)
|
|
510
|
+
if (!data) return null
|
|
511
|
+
|
|
512
|
+
if (data.methods?.[memberName]) {
|
|
513
|
+
return { kind: 'browser-member', id: found, member: memberName, memberType: 'method' }
|
|
514
|
+
}
|
|
515
|
+
if (data.getters?.[memberName]) {
|
|
516
|
+
return { kind: 'browser-member', id: found, member: memberName, memberType: 'getter' }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const allMembers = [
|
|
520
|
+
...Object.keys(data.methods || {}).map((m: string) => m + '()'),
|
|
521
|
+
...Object.keys(data.getters || {}),
|
|
522
|
+
].sort()
|
|
523
|
+
throw new DescribeError(
|
|
524
|
+
`"${memberName}" is not a known method or getter on ${found} (browser).\n\nAvailable members:\n ${allMembers.join(', ')}`
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// --- Private: Data getters ---
|
|
529
|
+
|
|
530
|
+
private getContainerData(sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
|
|
531
|
+
const container = this.container
|
|
532
|
+
|
|
533
|
+
if (sections.length === 0) {
|
|
534
|
+
const data = container.inspect()
|
|
535
|
+
return { json: data, text: container.inspectAsText(undefined, headingDepth) }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const data = container.inspect()
|
|
539
|
+
const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
|
|
540
|
+
const textParts: string[] = []
|
|
541
|
+
const jsonResult: Record<string, any> = {}
|
|
542
|
+
const h = '#'.repeat(headingDepth)
|
|
543
|
+
|
|
544
|
+
if (!noTitle) {
|
|
545
|
+
const className = data.className || 'Container'
|
|
546
|
+
textParts.push(`${h} ${className} (Container)`)
|
|
547
|
+
jsonResult.className = className
|
|
548
|
+
if (data.description) {
|
|
549
|
+
textParts.push(data.description)
|
|
550
|
+
jsonResult.description = data.description
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
for (const section of introspectionSections) {
|
|
555
|
+
textParts.push(container.inspectAsText(section, headingDepth))
|
|
556
|
+
jsonResult[section] = data[section]
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return { json: jsonResult, text: textParts.join('\n\n') }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private getHelperData(registryName: string, id: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
|
|
563
|
+
const registry = this.container[registryName]
|
|
564
|
+
const Ctor = registry.lookup(id)
|
|
565
|
+
const text = this.renderHelperText(Ctor, sections, noTitle, headingDepth)
|
|
566
|
+
|
|
567
|
+
let finalText = text
|
|
568
|
+
if (sections.length === 0 && !noTitle) {
|
|
569
|
+
const summary = this.buildHelperSummary(Ctor)
|
|
570
|
+
if (summary) {
|
|
571
|
+
const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
|
|
572
|
+
const idx = text.indexOf('\n' + sectionHeading)
|
|
573
|
+
if (idx >= 0) {
|
|
574
|
+
finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
json: this.renderHelperJson(Ctor, sections, noTitle),
|
|
581
|
+
text: finalText,
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private getMemberData(registryName: string, id: string, member: string, memberType: 'method' | 'getter', headingDepth: number): DescribeResult {
|
|
586
|
+
const registry = this.container[registryName]
|
|
587
|
+
const Ctor = registry.lookup(id)
|
|
588
|
+
const introspection = Ctor.introspect?.()
|
|
589
|
+
const h = '#'.repeat(headingDepth)
|
|
590
|
+
const hSub = '#'.repeat(headingDepth + 1)
|
|
591
|
+
|
|
592
|
+
if (memberType === 'method') {
|
|
593
|
+
const method = introspection?.methods?.[member] as MethodIntrospection | undefined
|
|
594
|
+
if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
|
|
595
|
+
|
|
596
|
+
const parts: string[] = []
|
|
597
|
+
parts.push(`${h} ${id}.${member}()`)
|
|
598
|
+
parts.push(`> method on **${introspection.className || id}**`)
|
|
599
|
+
if (method.description) parts.push(method.description)
|
|
600
|
+
|
|
601
|
+
const paramEntries = Object.entries(method.parameters || {})
|
|
602
|
+
if (paramEntries.length > 0) {
|
|
603
|
+
const paramLines = [`${hSub} Parameters`, '']
|
|
604
|
+
for (const [name, info] of paramEntries) {
|
|
605
|
+
const req = (method.required || []).includes(name) ? ' *(required)*' : ''
|
|
606
|
+
paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
|
|
607
|
+
if (info.properties) {
|
|
608
|
+
for (const [propName, propInfo] of Object.entries(info.properties)) {
|
|
609
|
+
paramLines.push(` - **${propName}** \`${propInfo.type}\`${propInfo.description ? ' — ' + propInfo.description : ''}`)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
parts.push(paramLines.join('\n'))
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (method.returns && method.returns !== 'void') {
|
|
617
|
+
parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (method.examples?.length) {
|
|
621
|
+
parts.push(`${hSub} Examples`)
|
|
622
|
+
for (const ex of method.examples) {
|
|
623
|
+
parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { json: { [member]: method, _helper: id, _type: 'method' }, text: parts.join('\n\n') }
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const getter = introspection?.getters?.[member] as GetterIntrospection | undefined
|
|
631
|
+
if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
|
|
632
|
+
|
|
633
|
+
const parts: string[] = []
|
|
634
|
+
parts.push(`${h} ${id}.${member}`)
|
|
635
|
+
parts.push(`> getter on **${introspection.className || id}** — returns \`${getter.returns || 'unknown'}\``)
|
|
636
|
+
if (getter.description) parts.push(getter.description)
|
|
637
|
+
|
|
638
|
+
if (getter.examples?.length) {
|
|
639
|
+
parts.push(`${hSub} Examples`)
|
|
640
|
+
for (const ex of getter.examples) {
|
|
641
|
+
parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return { json: { [member]: getter, _helper: id, _type: 'getter' }, text: parts.join('\n\n') }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private getBrowserHelperData(id: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): DescribeResult {
|
|
649
|
+
const data = this._browserData!.introspection.get(`features.${id}`)
|
|
650
|
+
if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
|
|
651
|
+
|
|
652
|
+
const text = this.renderBrowserHelperText(data, sections, noTitle, headingDepth)
|
|
653
|
+
|
|
654
|
+
let finalText = text
|
|
655
|
+
if (sections.length === 0 && !noTitle) {
|
|
656
|
+
const summary = this.buildBrowserHelperSummary(data)
|
|
657
|
+
if (summary) {
|
|
658
|
+
const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
|
|
659
|
+
const idx = text.indexOf('\n' + sectionHeading)
|
|
660
|
+
if (idx >= 0) {
|
|
661
|
+
finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
json: this.renderBrowserHelperJson(data, sections, noTitle),
|
|
668
|
+
text: finalText,
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private getBrowserMemberData(id: string, member: string, memberType: 'method' | 'getter', headingDepth: number): DescribeResult {
|
|
673
|
+
const data = this._browserData!.introspection.get(`features.${id}`)
|
|
674
|
+
if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
|
|
675
|
+
|
|
676
|
+
const h = '#'.repeat(headingDepth)
|
|
677
|
+
const hSub = '#'.repeat(headingDepth + 1)
|
|
678
|
+
|
|
679
|
+
if (memberType === 'method') {
|
|
680
|
+
const method = data.methods?.[member] as MethodIntrospection | undefined
|
|
681
|
+
if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
|
|
682
|
+
|
|
683
|
+
const parts: string[] = [`${h} ${id}.${member}() (browser)`]
|
|
684
|
+
parts.push(`> method on **${data.className || id}**`)
|
|
685
|
+
if (method.description) parts.push(method.description)
|
|
686
|
+
|
|
687
|
+
const paramEntries = Object.entries(method.parameters || {})
|
|
688
|
+
if (paramEntries.length > 0) {
|
|
689
|
+
const paramLines = [`${hSub} Parameters`, '']
|
|
690
|
+
for (const [name, info] of paramEntries) {
|
|
691
|
+
const req = (method.required || []).includes(name) ? ' *(required)*' : ''
|
|
692
|
+
paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
|
|
693
|
+
}
|
|
694
|
+
parts.push(paramLines.join('\n'))
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (method.returns && method.returns !== 'void') {
|
|
698
|
+
parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return { json: { [member]: method, _helper: id, _type: 'method', _platform: 'browser' }, text: parts.join('\n\n') }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const getter = data.getters?.[member] as GetterIntrospection | undefined
|
|
705
|
+
if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
|
|
706
|
+
|
|
707
|
+
const parts: string[] = [`${h} ${id}.${member} (browser)`]
|
|
708
|
+
parts.push(`> getter on **${data.className || id}** — returns \`${getter.returns || 'unknown'}\``)
|
|
709
|
+
if (getter.description) parts.push(getter.description)
|
|
710
|
+
|
|
711
|
+
return { json: { [member]: getter, _helper: id, _type: 'getter', _platform: 'browser' }, text: parts.join('\n\n') }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private getRegistryData(registryName: string, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number, platform: Platform): DescribeResult {
|
|
715
|
+
const includeNode = this.shouldIncludeNode(platform)
|
|
716
|
+
const includeBrowser = this.shouldIncludeBrowser(platform) && registryName === 'features' && this._browserData
|
|
717
|
+
|
|
718
|
+
const registry = this.container[registryName]
|
|
719
|
+
const nodeAvailable: string[] = includeNode ? registry.available : []
|
|
720
|
+
const browserAvailable: string[] = includeBrowser ? this._browserData!.available : []
|
|
721
|
+
|
|
722
|
+
const collidingIds = includeBrowser ? this._browserData!.collidingIds : new Set<string>()
|
|
723
|
+
const totalCount = nodeAvailable.length + browserAvailable.filter(id => !includeNode || !collidingIds.has(id)).length
|
|
724
|
+
|
|
725
|
+
if (totalCount === 0) {
|
|
726
|
+
return { json: {}, text: `No ${registryName} are registered.` }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (sections.length === 0) {
|
|
730
|
+
const h = '#'.repeat(headingDepth)
|
|
731
|
+
const hSub = '#'.repeat(headingDepth + 1)
|
|
732
|
+
const jsonResult: Record<string, any> = {}
|
|
733
|
+
const textParts: string[] = [`${h} Available ${registryName} (${totalCount})\n`]
|
|
734
|
+
|
|
735
|
+
if (includeNode) {
|
|
736
|
+
const baseClass = registry.baseClass
|
|
737
|
+
if (baseClass) {
|
|
738
|
+
const shared = this.collectSharedMembers(baseClass)
|
|
739
|
+
const label = registryName[0]!.toUpperCase() + registryName.slice(1).replace(/s$/, '')
|
|
740
|
+
|
|
741
|
+
if (shared.getters.length) textParts.push(`**Shared ${label} Getters:** ${shared.getters.join(', ')}\n`)
|
|
742
|
+
if (shared.methods.length) textParts.push(`**Shared ${label} Methods:** ${shared.methods.map(m => m + '()').join(', ')}\n`)
|
|
743
|
+
jsonResult._shared = { methods: shared.methods, getters: shared.getters }
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const baseClassRef = registry.baseClass
|
|
747
|
+
const sorted = [...nodeAvailable].sort((a, b) => {
|
|
748
|
+
const aCtor = registry.lookup(a)
|
|
749
|
+
const bCtor = registry.lookup(b)
|
|
750
|
+
const aIsDirect = !this.findIntermediateParent(aCtor, baseClassRef)
|
|
751
|
+
const bIsDirect = !this.findIntermediateParent(bCtor, baseClassRef)
|
|
752
|
+
if (aIsDirect && !bIsDirect) return -1
|
|
753
|
+
if (!aIsDirect && bIsDirect) return 1
|
|
754
|
+
return 0
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
for (const id of sorted) {
|
|
758
|
+
const Ctor = registry.lookup(id)
|
|
759
|
+
const introspection = Ctor.introspect?.()
|
|
760
|
+
const description = introspection?.description || Ctor.description || 'No description provided'
|
|
761
|
+
const summary = this.extractSummary(description)
|
|
762
|
+
const featureGetters = Object.keys(introspection?.getters || {}).sort()
|
|
763
|
+
const featureMethods = Object.keys(introspection?.methods || {}).sort()
|
|
764
|
+
const intermediate = this.findIntermediateParent(Ctor, baseClassRef)
|
|
765
|
+
|
|
766
|
+
const platformTag = includeBrowser && collidingIds.has(id) ? ' (node)' : ''
|
|
767
|
+
|
|
768
|
+
const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
|
|
769
|
+
if (intermediate) {
|
|
770
|
+
entryJson.extends = intermediate.name
|
|
771
|
+
entryJson.inheritedMethods = intermediate.methods
|
|
772
|
+
entryJson.inheritedGetters = intermediate.getters
|
|
773
|
+
}
|
|
774
|
+
if (platformTag) entryJson.platform = 'node'
|
|
775
|
+
jsonResult[id + (platformTag ? ':node' : '')] = entryJson
|
|
776
|
+
|
|
777
|
+
const extendsLine = intermediate ? `\n> extends ${intermediate.name}\n` : ''
|
|
778
|
+
const memberLines: string[] = []
|
|
779
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
780
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
781
|
+
if (intermediate) {
|
|
782
|
+
if (intermediate.getters.length) memberLines.push(` inherited getters: ${intermediate.getters.join(', ')}`)
|
|
783
|
+
if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
|
|
784
|
+
}
|
|
785
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
786
|
+
textParts.push(`${hSub} ${id}${platformTag}${extendsLine}\n${summary}\n${memberBlock}`)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (includeBrowser) {
|
|
791
|
+
for (const id of browserAvailable.sort()) {
|
|
792
|
+
if (includeNode && collidingIds.has(id)) {
|
|
793
|
+
const data = this._browserData!.introspection.get(`features.${id}`)
|
|
794
|
+
if (!data) continue
|
|
795
|
+
const summary = this.extractSummary(data.description || 'No description provided')
|
|
796
|
+
const featureGetters = Object.keys(data.getters || {}).sort()
|
|
797
|
+
const featureMethods = Object.keys(data.methods || {}).sort()
|
|
798
|
+
|
|
799
|
+
jsonResult[id + ':browser'] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
|
|
800
|
+
|
|
801
|
+
const memberLines: string[] = []
|
|
802
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
803
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
804
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
805
|
+
textParts.push(`${hSub} ${id} (browser)\n${summary}\n${memberBlock}`)
|
|
806
|
+
continue
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const data = this._browserData!.introspection.get(`features.${id}`)
|
|
810
|
+
if (!data) continue
|
|
811
|
+
const summary = this.extractSummary(data.description || 'No description provided')
|
|
812
|
+
const featureGetters = Object.keys(data.getters || {}).sort()
|
|
813
|
+
const featureMethods = Object.keys(data.methods || {}).sort()
|
|
814
|
+
|
|
815
|
+
const platformTag = !includeNode ? '' : ' (browser)'
|
|
816
|
+
jsonResult[id] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
|
|
817
|
+
|
|
818
|
+
const memberLines: string[] = []
|
|
819
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
820
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
821
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
822
|
+
textParts.push(`${hSub} ${id}${platformTag}\n${summary}\n${memberBlock}`)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return { json: jsonResult, text: textParts.join('\n') }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Sections specified: render each helper in detail
|
|
830
|
+
const jsonResult: Record<string, any> = {}
|
|
831
|
+
const textParts: string[] = []
|
|
832
|
+
|
|
833
|
+
if (includeNode) {
|
|
834
|
+
for (const id of nodeAvailable) {
|
|
835
|
+
const Ctor = registry.lookup(id)
|
|
836
|
+
jsonResult[id] = this.renderHelperJson(Ctor, sections, noTitle)
|
|
837
|
+
textParts.push(this.renderHelperText(Ctor, sections, noTitle, headingDepth))
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (includeBrowser) {
|
|
842
|
+
for (const id of browserAvailable) {
|
|
843
|
+
if (includeNode && collidingIds.has(id)) continue
|
|
844
|
+
const data = this._browserData!.introspection.get(`features.${id}`)
|
|
845
|
+
if (!data) continue
|
|
846
|
+
jsonResult[id] = this.renderBrowserHelperJson(data, sections, noTitle)
|
|
847
|
+
textParts.push(this.renderBrowserHelperText(data, sections, noTitle, headingDepth))
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// --- Private: Rendering helpers ---
|
|
855
|
+
|
|
856
|
+
private renderTitle(Ctor: any, headingDepth = 1): string {
|
|
857
|
+
const data = Ctor.introspect?.()
|
|
858
|
+
const id = data?.id || Ctor.shortcut || Ctor.name
|
|
859
|
+
const className = data?.className || Ctor.name
|
|
860
|
+
const h = '#'.repeat(headingDepth)
|
|
861
|
+
return className ? `${h} ${className} (${id})` : `${h} ${id}`
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private renderHelperText(Ctor: any, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): string {
|
|
865
|
+
if (sections.length === 0) {
|
|
866
|
+
if (noTitle) {
|
|
867
|
+
const data = Ctor.introspect?.()
|
|
868
|
+
if (!data) return 'No introspection data available.'
|
|
869
|
+
const parts: string[] = [data.description]
|
|
870
|
+
const text = Ctor.introspectAsText?.(headingDepth)
|
|
871
|
+
if (text) {
|
|
872
|
+
const lines = text.split('\n')
|
|
873
|
+
const headingPrefix = '#'.repeat(headingDepth + 1) + ' '
|
|
874
|
+
let startIdx = 0
|
|
875
|
+
for (let i = 0; i < lines.length; i++) {
|
|
876
|
+
if (i > 0 && lines[i]!.startsWith(headingPrefix)) {
|
|
877
|
+
startIdx = i
|
|
878
|
+
break
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (startIdx > 0) {
|
|
882
|
+
parts.length = 0
|
|
883
|
+
parts.push(data.description)
|
|
884
|
+
parts.push(lines.slice(startIdx).join('\n'))
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return parts.join('\n\n')
|
|
888
|
+
}
|
|
889
|
+
return Ctor.introspectAsText?.(headingDepth) ?? `${this.renderTitle(Ctor, headingDepth)}\n\nNo introspection data available.`
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
|
|
893
|
+
const parts: string[] = []
|
|
894
|
+
|
|
895
|
+
if (!noTitle) {
|
|
896
|
+
const data = Ctor.introspect?.()
|
|
897
|
+
parts.push(this.renderTitle(Ctor, headingDepth))
|
|
898
|
+
if (data?.description) parts.push(data.description)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
for (const section of introspectionSections) {
|
|
902
|
+
const text = Ctor.introspectAsText?.(section, headingDepth)
|
|
903
|
+
if (text) parts.push(text)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return parts.join('\n\n') || `${noTitle ? '' : this.renderTitle(Ctor, headingDepth) + '\n\n'}No introspection data available.`
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private renderHelperJson(Ctor: any, sections: (IntrospectionSection | 'description')[], noTitle: boolean): any {
|
|
910
|
+
if (sections.length === 0) return Ctor.introspect?.() ?? {}
|
|
911
|
+
|
|
912
|
+
const data = Ctor.introspect?.() ?? {}
|
|
913
|
+
const result: Record<string, any> = {}
|
|
914
|
+
|
|
915
|
+
if (!noTitle) {
|
|
916
|
+
result.id = data.id
|
|
917
|
+
if (data.className) result.className = data.className
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
for (const section of sections) {
|
|
921
|
+
if (section === 'description') {
|
|
922
|
+
result.id = data.id
|
|
923
|
+
if (data.className) result.className = data.className
|
|
924
|
+
result.description = data.description
|
|
925
|
+
} else if (section === 'usage') {
|
|
926
|
+
result.usage = { shortcut: data.shortcut, options: data.options }
|
|
927
|
+
} else {
|
|
928
|
+
const sectionData = Ctor.introspect?.(section)
|
|
929
|
+
if (sectionData) result[section] = sectionData[section]
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return result
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private renderBrowserHelperText(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle: boolean, headingDepth: number): string {
|
|
937
|
+
const h = '#'.repeat(headingDepth)
|
|
938
|
+
const className = data.className || data.id
|
|
939
|
+
|
|
940
|
+
if (sections.length === 0) {
|
|
941
|
+
const body = presentIntrospectionAsMarkdown(data, headingDepth)
|
|
942
|
+
if (noTitle) {
|
|
943
|
+
const lines = body.split('\n')
|
|
944
|
+
const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
|
|
945
|
+
const firstSectionIdx = lines.findIndex((l, i) => i > 0 && l.startsWith(sectionHeading))
|
|
946
|
+
const desc = data.description ? data.description + '\n\n' : ''
|
|
947
|
+
if (firstSectionIdx > 0) return desc + lines.slice(firstSectionIdx).join('\n')
|
|
948
|
+
return desc.trim() || 'No introspection data available.'
|
|
949
|
+
}
|
|
950
|
+
return body
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const parts: string[] = []
|
|
954
|
+
if (!noTitle) {
|
|
955
|
+
parts.push(`${h} ${className} (${data.id})`)
|
|
956
|
+
if (data.description) parts.push(data.description)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
|
|
960
|
+
for (const section of introspectionSections) {
|
|
961
|
+
const text = presentIntrospectionAsMarkdown(data, headingDepth, section)
|
|
962
|
+
if (text) parts.push(text)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return parts.join('\n\n') || `${noTitle ? '' : `${h} ${className}\n\n`}No introspection data available.`
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private renderBrowserHelperJson(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle: boolean): any {
|
|
969
|
+
if (sections.length === 0) return data
|
|
970
|
+
|
|
971
|
+
const result: Record<string, any> = {}
|
|
972
|
+
if (!noTitle) {
|
|
973
|
+
result.id = data.id
|
|
974
|
+
if (data.className) result.className = data.className
|
|
975
|
+
}
|
|
976
|
+
for (const section of sections) {
|
|
977
|
+
if (section === 'description') {
|
|
978
|
+
result.id = data.id
|
|
979
|
+
if (data.className) result.className = data.className
|
|
980
|
+
result.description = data.description
|
|
981
|
+
} else if (section === 'usage') {
|
|
982
|
+
result.usage = { shortcut: data.shortcut, options: data.options }
|
|
983
|
+
} else {
|
|
984
|
+
result[section] = (data as any)[section]
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return result
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// --- Private: Summary builders ---
|
|
991
|
+
|
|
992
|
+
private extractSummary(description: string): string {
|
|
993
|
+
const cut = description.search(/\s\*\*[A-Z][\w\s]+:\*\*|```|^\s*[-*]\s/m)
|
|
994
|
+
const text = cut > 0 ? description.slice(0, cut).trim() : description
|
|
995
|
+
if (text.length <= 300) return text
|
|
996
|
+
const sentenceEnd = text.lastIndexOf('. ', 300)
|
|
997
|
+
if (sentenceEnd > 100) return text.slice(0, sentenceEnd + 1)
|
|
998
|
+
return text.slice(0, 300).trim() + '...'
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private buildHelperSummary(Ctor: any): string {
|
|
1002
|
+
const introspection = Ctor.introspect?.()
|
|
1003
|
+
const ownMethods = Object.keys(introspection?.methods || {}).sort()
|
|
1004
|
+
const ownGetters = Object.keys(introspection?.getters || {}).sort()
|
|
1005
|
+
|
|
1006
|
+
const chain: any[] = []
|
|
1007
|
+
let current = Object.getPrototypeOf(Ctor)
|
|
1008
|
+
while (current && current.name && !BASE_CLASS_NAMES.has(current.name) && current !== Function.prototype) {
|
|
1009
|
+
chain.push(current)
|
|
1010
|
+
current = Object.getPrototypeOf(current)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const lines: string[] = []
|
|
1014
|
+
if (chain.length > 0) lines.push(`> extends ${chain[0].name}`)
|
|
1015
|
+
if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
|
|
1016
|
+
if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
|
|
1017
|
+
|
|
1018
|
+
for (const parent of chain) {
|
|
1019
|
+
const parentIntrospection = parent.introspect?.()
|
|
1020
|
+
const inheritedMethods = Object.keys(parentIntrospection?.methods || {}).sort()
|
|
1021
|
+
const inheritedGetters = Object.keys(parentIntrospection?.getters || {}).sort()
|
|
1022
|
+
if (inheritedGetters.length) lines.push(`inherited getters (${parent.name}): ${inheritedGetters.join(', ')}`)
|
|
1023
|
+
if (inheritedMethods.length) lines.push(`inherited methods (${parent.name}): ${inheritedMethods.map(m => m + '()').join(', ')}`)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return lines.join('\n')
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private buildBrowserHelperSummary(data: HelperIntrospection): string {
|
|
1030
|
+
const ownMethods = Object.keys(data.methods || {}).sort()
|
|
1031
|
+
const ownGetters = Object.keys(data.getters || {}).sort()
|
|
1032
|
+
const lines: string[] = []
|
|
1033
|
+
if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
|
|
1034
|
+
if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
|
|
1035
|
+
return lines.join('\n')
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// --- Private: Prototype chain helpers ---
|
|
1039
|
+
|
|
1040
|
+
private collectSharedMembers(baseClass: any): { methods: string[]; getters: string[] } {
|
|
1041
|
+
const methods: string[] = []
|
|
1042
|
+
const getters: string[] = []
|
|
1043
|
+
|
|
1044
|
+
let proto = baseClass?.prototype
|
|
1045
|
+
while (proto && proto.constructor.name !== 'Object') {
|
|
1046
|
+
for (const k of Object.getOwnPropertyNames(proto)) {
|
|
1047
|
+
if (k === 'constructor' || k.startsWith('_')) continue
|
|
1048
|
+
const desc = Object.getOwnPropertyDescriptor(proto, k)
|
|
1049
|
+
if (!desc) continue
|
|
1050
|
+
if (desc.get && !getters.includes(k)) getters.push(k)
|
|
1051
|
+
else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
|
|
1052
|
+
}
|
|
1053
|
+
proto = Object.getPrototypeOf(proto)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return { methods: methods.sort(), getters: getters.sort() }
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private findIntermediateParent(Ctor: any, baseClass: any): { name: string; methods: string[]; getters: string[] } | null {
|
|
1060
|
+
if (!baseClass) return null
|
|
1061
|
+
|
|
1062
|
+
let parent = Object.getPrototypeOf(Ctor)
|
|
1063
|
+
if (!parent || parent === baseClass) return null
|
|
1064
|
+
|
|
1065
|
+
const chain: any[] = []
|
|
1066
|
+
let current = parent
|
|
1067
|
+
while (current && current !== baseClass && current !== Function.prototype) {
|
|
1068
|
+
chain.push(current)
|
|
1069
|
+
current = Object.getPrototypeOf(current)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (chain.length === 0 || current !== baseClass) return null
|
|
1073
|
+
|
|
1074
|
+
const intermediate = chain[0]
|
|
1075
|
+
const methods: string[] = []
|
|
1076
|
+
const getters: string[] = []
|
|
1077
|
+
|
|
1078
|
+
const proto = intermediate?.prototype
|
|
1079
|
+
if (proto) {
|
|
1080
|
+
for (const k of Object.getOwnPropertyNames(proto)) {
|
|
1081
|
+
if (k === 'constructor' || k.startsWith('_')) continue
|
|
1082
|
+
const desc = Object.getOwnPropertyDescriptor(proto, k)
|
|
1083
|
+
if (!desc) continue
|
|
1084
|
+
if (desc.get && !getters.includes(k)) getters.push(k)
|
|
1085
|
+
else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
name: intermediate.name,
|
|
1091
|
+
methods: methods.sort(),
|
|
1092
|
+
getters: getters.sort(),
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export { DescribeError, SECTION_FLAGS }
|
|
1098
|
+
export type { ResolvedTarget, Platform, DescribeOptions, DescribeResult, BrowserFeatureData }
|