@soederpop/luca 0.0.19 → 0.0.20
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/bootstrap/generated.ts +1 -1
- package/src/commands/describe.ts +557 -94
- package/src/introspection/generated.agi.ts +1 -1
- package/src/introspection/generated.node.ts +1176 -1176
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +7 -0
- package/src/registry.ts +12 -1
- package/src/scaffolds/generated.ts +1 -1
package/src/commands/describe.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { z } from 'zod'
|
|
|
2
2
|
import { commands } from '../command.js'
|
|
3
3
|
import { CommandOptionsSchema } from '../schemas/base.js'
|
|
4
4
|
import type { ContainerContext } from '../container.js'
|
|
5
|
-
import type { IntrospectionSection, MethodIntrospection, GetterIntrospection } from '../introspection/index.js'
|
|
5
|
+
import type { IntrospectionSection, MethodIntrospection, GetterIntrospection, HelperIntrospection } from '../introspection/index.js'
|
|
6
|
+
import { __INTROSPECTION__, __BROWSER_INTROSPECTION__ } from '../introspection/index.js'
|
|
7
|
+
import { features } from '../feature.js'
|
|
8
|
+
import { presentIntrospectionAsMarkdown } from '../helper.js'
|
|
6
9
|
|
|
7
10
|
declare module '../command.js' {
|
|
8
11
|
interface AvailableCommands {
|
|
@@ -61,13 +64,18 @@ export const argsSchema = CommandOptionsSchema.extend({
|
|
|
61
64
|
'only-env-vars': z.boolean().default(false).describe('Show only the envVars section'),
|
|
62
65
|
'only-envvars': z.boolean().default(false).describe('Show only the envVars section'),
|
|
63
66
|
'only-examples': z.boolean().default(false).describe('Show only the examples section'),
|
|
67
|
+
platform: z.enum(['browser', 'web', 'server', 'node', 'all']).default('all').describe('Which platform features to show: browser/web, server/node, or all'),
|
|
64
68
|
})
|
|
65
69
|
|
|
70
|
+
type Platform = 'browser' | 'server' | 'node' | 'all'
|
|
71
|
+
|
|
66
72
|
type ResolvedTarget =
|
|
67
73
|
| { kind: 'container' }
|
|
68
74
|
| { kind: 'registry'; name: RegistryName }
|
|
69
75
|
| { kind: 'helper'; registry: RegistryName; id: string }
|
|
70
76
|
| { kind: 'member'; registry: RegistryName; id: string; member: string; memberType: 'method' | 'getter' }
|
|
77
|
+
| { kind: 'browser-helper'; id: string }
|
|
78
|
+
| { kind: 'browser-member'; id: string; member: string; memberType: 'method' | 'getter' }
|
|
71
79
|
|
|
72
80
|
class DescribeError extends Error {
|
|
73
81
|
constructor(message: string) {
|
|
@@ -76,6 +84,303 @@ class DescribeError extends Error {
|
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
// --- Browser feature loading ---
|
|
88
|
+
|
|
89
|
+
const WEB_FEATURE_IDS = ['speech', 'voice', 'assetLoader', 'network', 'vault', 'vm', 'esbuild', 'helpers', 'containerLink']
|
|
90
|
+
|
|
91
|
+
type BrowserFeatureData = {
|
|
92
|
+
introspection: Map<string, HelperIntrospection>
|
|
93
|
+
constructors: Map<string, any>
|
|
94
|
+
available: string[]
|
|
95
|
+
collidingIds: Set<string>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let _browserData: BrowserFeatureData | null = null
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Load web/browser feature introspection data into a separate map.
|
|
102
|
+
* Imports the web feature class files to get full Zod-derived data,
|
|
103
|
+
* then restores the node registry and introspection to their original state.
|
|
104
|
+
*/
|
|
105
|
+
async function loadBrowserFeatures(): Promise<BrowserFeatureData> {
|
|
106
|
+
if (_browserData) return _browserData
|
|
107
|
+
|
|
108
|
+
// Snapshot current node state
|
|
109
|
+
const nodeFeatureIds = new Set(features.available)
|
|
110
|
+
const nodeIntrospection = new Map<string, HelperIntrospection>()
|
|
111
|
+
const nodeConstructors = new Map<string, any>()
|
|
112
|
+
|
|
113
|
+
for (const id of nodeFeatureIds) {
|
|
114
|
+
const data = __INTROSPECTION__.get(`features.${id}`)
|
|
115
|
+
if (data) nodeIntrospection.set(`features.${id}`, structuredClone(data))
|
|
116
|
+
try { nodeConstructors.set(id, features.lookup(id)) } catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Import generated web build-time data (descriptions, methods, getters from AST)
|
|
120
|
+
await import('../introspection/generated.web.js')
|
|
121
|
+
|
|
122
|
+
// Import web feature class files (triggers Feature.register → interceptRegistration → Zod data)
|
|
123
|
+
await Promise.all([
|
|
124
|
+
import('../web/features/speech.js'),
|
|
125
|
+
import('../web/features/voice-recognition.js'),
|
|
126
|
+
import('../web/features/asset-loader.js'),
|
|
127
|
+
import('../web/features/network.js'),
|
|
128
|
+
import('../web/features/vault.js'),
|
|
129
|
+
import('../web/features/vm.js'),
|
|
130
|
+
import('../web/features/esbuild.js'),
|
|
131
|
+
import('../web/features/helpers.js'),
|
|
132
|
+
import('../web/features/container-link.js'),
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
// Capture browser introspection data and constructors
|
|
136
|
+
const browserIntrospection = new Map<string, HelperIntrospection>()
|
|
137
|
+
const browserConstructors = new Map<string, any>()
|
|
138
|
+
const collidingIds = new Set<string>()
|
|
139
|
+
|
|
140
|
+
for (const id of WEB_FEATURE_IDS) {
|
|
141
|
+
const key = `features.${id}`
|
|
142
|
+
const data = __INTROSPECTION__.get(key)
|
|
143
|
+
if (data) browserIntrospection.set(key, structuredClone(data))
|
|
144
|
+
try { browserConstructors.set(id, features.lookup(id)) } catch {}
|
|
145
|
+
if (nodeFeatureIds.has(id)) collidingIds.add(id)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Restore node registry: re-register all node constructors
|
|
149
|
+
for (const [id, ctor] of nodeConstructors) {
|
|
150
|
+
features.register(id, ctor)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fully restore node introspection (overwrite whatever interceptRegistration did during re-register)
|
|
154
|
+
for (const [key, data] of nodeIntrospection) {
|
|
155
|
+
__INTROSPECTION__.set(key, data)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Clean up: remove web-only entries from __INTROSPECTION__ and the registry
|
|
159
|
+
for (const id of WEB_FEATURE_IDS) {
|
|
160
|
+
const key = `features.${id}`
|
|
161
|
+
if (!nodeIntrospection.has(key)) {
|
|
162
|
+
__INTROSPECTION__.delete(key)
|
|
163
|
+
}
|
|
164
|
+
if (!nodeFeatureIds.has(id)) {
|
|
165
|
+
features.unregister(id)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Store in __BROWSER_INTROSPECTION__ for other potential consumers
|
|
170
|
+
for (const [key, data] of browserIntrospection) {
|
|
171
|
+
__BROWSER_INTROSPECTION__.set(key, data)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_browserData = {
|
|
175
|
+
introspection: browserIntrospection,
|
|
176
|
+
constructors: browserConstructors,
|
|
177
|
+
available: WEB_FEATURE_IDS.filter(id => browserIntrospection.has(`features.${id}`)),
|
|
178
|
+
collidingIds,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return _browserData
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Render a browser feature's introspection data directly (no Ctor needed). */
|
|
185
|
+
function renderBrowserHelperText(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): string {
|
|
186
|
+
const h = '#'.repeat(headingDepth)
|
|
187
|
+
const className = data.className || data.id
|
|
188
|
+
|
|
189
|
+
if (sections.length === 0) {
|
|
190
|
+
const body = presentIntrospectionAsMarkdown(data, headingDepth)
|
|
191
|
+
if (noTitle) {
|
|
192
|
+
// Strip the title heading and return description + sections
|
|
193
|
+
const lines = body.split('\n')
|
|
194
|
+
const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
|
|
195
|
+
const firstSectionIdx = lines.findIndex((l, i) => i > 0 && l.startsWith(sectionHeading))
|
|
196
|
+
const desc = data.description ? data.description + '\n\n' : ''
|
|
197
|
+
if (firstSectionIdx > 0) {
|
|
198
|
+
return desc + lines.slice(firstSectionIdx).join('\n')
|
|
199
|
+
}
|
|
200
|
+
// No subsections — just return the description
|
|
201
|
+
return desc.trim() || 'No introspection data available.'
|
|
202
|
+
}
|
|
203
|
+
return body
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parts: string[] = []
|
|
207
|
+
if (!noTitle) {
|
|
208
|
+
parts.push(`${h} ${className} (${data.id})`)
|
|
209
|
+
if (data.description) parts.push(data.description)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
|
|
213
|
+
for (const section of introspectionSections) {
|
|
214
|
+
const text = presentIntrospectionAsMarkdown(data, headingDepth, section)
|
|
215
|
+
if (text) parts.push(text)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return parts.join('\n\n') || `${noTitle ? '' : `${h} ${className}\n\n`}No introspection data available.`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderBrowserHelperJson(data: HelperIntrospection, sections: (IntrospectionSection | 'description')[], noTitle = false): any {
|
|
222
|
+
if (sections.length === 0) return data
|
|
223
|
+
|
|
224
|
+
const result: Record<string, any> = {}
|
|
225
|
+
if (!noTitle) {
|
|
226
|
+
result.id = data.id
|
|
227
|
+
if (data.className) result.className = data.className
|
|
228
|
+
}
|
|
229
|
+
for (const section of sections) {
|
|
230
|
+
if (section === 'description') {
|
|
231
|
+
result.id = data.id
|
|
232
|
+
if (data.className) result.className = data.className
|
|
233
|
+
result.description = data.description
|
|
234
|
+
} else if (section === 'usage') {
|
|
235
|
+
result.usage = { shortcut: data.shortcut, options: data.options }
|
|
236
|
+
} else {
|
|
237
|
+
result[section] = (data as any)[section]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Build a concise summary for a browser feature from introspection data. */
|
|
244
|
+
function buildBrowserHelperSummary(data: HelperIntrospection): string {
|
|
245
|
+
const ownMethods = Object.keys(data.methods || {}).sort()
|
|
246
|
+
const ownGetters = Object.keys(data.getters || {}).sort()
|
|
247
|
+
const lines: string[] = []
|
|
248
|
+
if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
|
|
249
|
+
if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
|
|
250
|
+
return lines.join('\n')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getBrowserHelperData(id: string, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
|
|
254
|
+
const data = _browserData!.introspection.get(`features.${id}`)
|
|
255
|
+
if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
|
|
256
|
+
|
|
257
|
+
const text = renderBrowserHelperText(data, sections, noTitle, headingDepth)
|
|
258
|
+
|
|
259
|
+
// Inject summary after title for full renders
|
|
260
|
+
let finalText = text
|
|
261
|
+
if (sections.length === 0 && !noTitle) {
|
|
262
|
+
const summary = buildBrowserHelperSummary(data)
|
|
263
|
+
if (summary) {
|
|
264
|
+
const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
|
|
265
|
+
const idx = text.indexOf('\n' + sectionHeading)
|
|
266
|
+
if (idx >= 0) {
|
|
267
|
+
finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
json: renderBrowserHelperJson(data, sections, noTitle),
|
|
274
|
+
text: finalText,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getBrowserMemberData(id: string, member: string, memberType: 'method' | 'getter', headingDepth = 1): { json: any; text: string } {
|
|
279
|
+
const data = _browserData!.introspection.get(`features.${id}`)
|
|
280
|
+
if (!data) return { json: {}, text: `No browser introspection data for ${id}` }
|
|
281
|
+
|
|
282
|
+
const h = '#'.repeat(headingDepth)
|
|
283
|
+
const hSub = '#'.repeat(headingDepth + 1)
|
|
284
|
+
|
|
285
|
+
if (memberType === 'method') {
|
|
286
|
+
const method = data.methods?.[member] as MethodIntrospection | undefined
|
|
287
|
+
if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
|
|
288
|
+
|
|
289
|
+
const parts: string[] = [`${h} ${id}.${member}() (browser)`]
|
|
290
|
+
parts.push(`> method on **${data.className || id}**`)
|
|
291
|
+
if (method.description) parts.push(method.description)
|
|
292
|
+
|
|
293
|
+
const paramEntries = Object.entries(method.parameters || {})
|
|
294
|
+
if (paramEntries.length > 0) {
|
|
295
|
+
const paramLines = [`${hSub} Parameters`, '']
|
|
296
|
+
for (const [name, info] of paramEntries) {
|
|
297
|
+
const req = (method.required || []).includes(name) ? ' *(required)*' : ''
|
|
298
|
+
paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
|
|
299
|
+
}
|
|
300
|
+
parts.push(paramLines.join('\n'))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (method.returns && method.returns !== 'void') {
|
|
304
|
+
parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { json: { [member]: method, _helper: id, _type: 'method', _platform: 'browser' }, text: parts.join('\n\n') }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const getter = data.getters?.[member] as GetterIntrospection | undefined
|
|
311
|
+
if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
|
|
312
|
+
|
|
313
|
+
const parts: string[] = [`${h} ${id}.${member} (browser)`]
|
|
314
|
+
parts.push(`> getter on **${data.className || id}** — returns \`${getter.returns || 'unknown'}\``)
|
|
315
|
+
if (getter.description) parts.push(getter.description)
|
|
316
|
+
|
|
317
|
+
return { json: { [member]: getter, _helper: id, _type: 'getter', _platform: 'browser' }, text: parts.join('\n\n') }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getBrowserRegistryData(sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
|
|
321
|
+
const browserData = _browserData!
|
|
322
|
+
const available = browserData.available
|
|
323
|
+
|
|
324
|
+
if (available.length === 0) {
|
|
325
|
+
return { json: {}, text: 'No browser features are registered.' }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (sections.length === 0) {
|
|
329
|
+
const h = '#'.repeat(headingDepth)
|
|
330
|
+
const hSub = '#'.repeat(headingDepth + 1)
|
|
331
|
+
const jsonResult: Record<string, any> = {}
|
|
332
|
+
const textParts: string[] = [`${h} Available browser features (${available.length})\n`]
|
|
333
|
+
|
|
334
|
+
for (const id of available.sort()) {
|
|
335
|
+
const data = browserData.introspection.get(`features.${id}`)
|
|
336
|
+
if (!data) continue
|
|
337
|
+
|
|
338
|
+
const summary = extractSummary(data.description || 'No description provided')
|
|
339
|
+
const featureGetters = Object.keys(data.getters || {}).sort()
|
|
340
|
+
const featureMethods = Object.keys(data.methods || {}).sort()
|
|
341
|
+
|
|
342
|
+
jsonResult[id] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
|
|
343
|
+
|
|
344
|
+
const memberLines: string[] = []
|
|
345
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
346
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
347
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
348
|
+
|
|
349
|
+
textParts.push(`${hSub} ${id}\n${summary}\n${memberBlock}`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { json: jsonResult, text: textParts.join('\n') }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Sections specified: render each helper in detail
|
|
356
|
+
const jsonResult: Record<string, any> = {}
|
|
357
|
+
const textParts: string[] = []
|
|
358
|
+
for (const id of available) {
|
|
359
|
+
const data = browserData.introspection.get(`features.${id}`)
|
|
360
|
+
if (!data) continue
|
|
361
|
+
jsonResult[id] = renderBrowserHelperJson(data, sections, noTitle)
|
|
362
|
+
textParts.push(renderBrowserHelperText(data, sections, noTitle, headingDepth))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizePlatform(p: string): Platform {
|
|
369
|
+
if (p === 'node') return 'server'
|
|
370
|
+
if (p === 'web') return 'browser'
|
|
371
|
+
return p as Platform
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function shouldIncludeNode(platform: Platform): boolean {
|
|
375
|
+
return platform === 'server' || platform === 'node' || platform === 'all'
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function shouldIncludeBrowser(platform: Platform): boolean {
|
|
379
|
+
return platform === 'browser' || platform === 'all'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- End browser feature loading ---
|
|
383
|
+
|
|
79
384
|
/**
|
|
80
385
|
* Extract a short summary from a potentially long description string.
|
|
81
386
|
* Takes text up to the first markdown heading, bullet list, or code block,
|
|
@@ -153,16 +458,50 @@ function resolveHelperMember(helperName: string, memberName: string, container:
|
|
|
153
458
|
return null
|
|
154
459
|
}
|
|
155
460
|
|
|
461
|
+
/** Find a browser feature by normalized name. */
|
|
462
|
+
function fuzzyFindBrowser(input: string): string | undefined {
|
|
463
|
+
if (!_browserData) return undefined
|
|
464
|
+
const norm = normalize(input)
|
|
465
|
+
return _browserData.available.find(id => normalize(id) === norm)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Try to resolve "browserHelper.member" for browser features. */
|
|
469
|
+
function resolveBrowserHelperMember(helperName: string, memberName: string): ResolvedTarget | null {
|
|
470
|
+
if (!_browserData) return null
|
|
471
|
+
const found = fuzzyFindBrowser(helperName)
|
|
472
|
+
if (!found) return null
|
|
473
|
+
|
|
474
|
+
const data = _browserData.introspection.get(`features.${found}`)
|
|
475
|
+
if (!data) return null
|
|
476
|
+
|
|
477
|
+
if (data.methods?.[memberName]) {
|
|
478
|
+
return { kind: 'browser-member', id: found, member: memberName, memberType: 'method' }
|
|
479
|
+
}
|
|
480
|
+
if (data.getters?.[memberName]) {
|
|
481
|
+
return { kind: 'browser-member', id: found, member: memberName, memberType: 'getter' }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const allMembers = [
|
|
485
|
+
...Object.keys(data.methods || {}).map((m: string) => m + '()'),
|
|
486
|
+
...Object.keys(data.getters || {}),
|
|
487
|
+
].sort()
|
|
488
|
+
throw new DescribeError(
|
|
489
|
+
`"${memberName}" is not a known method or getter on ${found} (browser).\n\nAvailable members:\n ${allMembers.join(', ')}`
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
156
493
|
/**
|
|
157
494
|
* Parse a single target string into a resolved target.
|
|
158
495
|
* Accepts: "container", "features", "features.fs", "fs", "ui.banner", etc.
|
|
159
496
|
*/
|
|
160
|
-
function resolveTarget(target: string, container: any): ResolvedTarget {
|
|
497
|
+
function resolveTarget(target: string, container: any, platform: Platform): ResolvedTarget[] {
|
|
161
498
|
const lower = target.toLowerCase()
|
|
499
|
+
const includeNode = shouldIncludeNode(platform)
|
|
500
|
+
const includeBrowser = shouldIncludeBrowser(platform)
|
|
162
501
|
|
|
163
502
|
// "container" or "self"
|
|
164
503
|
if (lower === 'container' || lower === 'self') {
|
|
165
|
-
return { kind: 'container' }
|
|
504
|
+
return [{ kind: 'container' }]
|
|
166
505
|
}
|
|
167
506
|
|
|
168
507
|
// Registry name: "features", "clients", "servers", "commands", "endpoints"
|
|
@@ -170,7 +509,7 @@ function resolveTarget(target: string, container: any): ResolvedTarget {
|
|
|
170
509
|
(r) => r === lower || r === lower + 's' || r.replace(/s$/, '') === lower
|
|
171
510
|
)
|
|
172
511
|
if (registryMatch && !target.includes('.')) {
|
|
173
|
-
return { kind: 'registry', name: registryMatch }
|
|
512
|
+
return [{ kind: 'registry', name: registryMatch }]
|
|
174
513
|
}
|
|
175
514
|
|
|
176
515
|
// Qualified name: "features.fs", "clients.rest", etc.
|
|
@@ -182,53 +521,103 @@ function resolveTarget(target: string, container: any): ResolvedTarget {
|
|
|
182
521
|
)
|
|
183
522
|
|
|
184
523
|
if (registry) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
524
|
+
const results: ResolvedTarget[] = []
|
|
525
|
+
|
|
526
|
+
if (includeNode) {
|
|
527
|
+
const reg = container[registry]
|
|
528
|
+
const resolved = fuzzyFind(reg, id)
|
|
529
|
+
if (resolved) results.push({ kind: 'helper', registry, id: resolved })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (includeBrowser && registry === 'features') {
|
|
533
|
+
const browserFound = fuzzyFindBrowser(id)
|
|
534
|
+
if (browserFound) results.push({ kind: 'browser-helper', id: browserFound })
|
|
189
535
|
}
|
|
190
|
-
|
|
536
|
+
|
|
537
|
+
if (results.length === 0) {
|
|
538
|
+
const reg = container[registry]
|
|
539
|
+
const availableMsg = includeNode ? reg.available.join(', ') : ''
|
|
540
|
+
const browserMsg = includeBrowser && _browserData ? _browserData.available.join(', ') : ''
|
|
541
|
+
const combined = [availableMsg, browserMsg].filter(Boolean).join(', ')
|
|
542
|
+
throw new DescribeError(`"${id}" is not registered in ${registry}. Available: ${combined}`)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return results
|
|
191
546
|
}
|
|
192
547
|
|
|
193
548
|
// Not a registry prefix — try "helper.member" (e.g. "ui.banner", "fs.readFile")
|
|
194
549
|
const helperName = prefix!
|
|
195
550
|
const memberName = rest.join('.')
|
|
196
|
-
const
|
|
197
|
-
|
|
551
|
+
const results: ResolvedTarget[] = []
|
|
552
|
+
|
|
553
|
+
if (includeNode) {
|
|
554
|
+
const memberResult = resolveHelperMember(helperName, memberName, container)
|
|
555
|
+
if (memberResult) results.push(memberResult)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (includeBrowser) {
|
|
559
|
+
try {
|
|
560
|
+
const browserResult = resolveBrowserHelperMember(helperName, memberName)
|
|
561
|
+
if (browserResult) results.push(browserResult)
|
|
562
|
+
} catch (e) {
|
|
563
|
+
if (results.length === 0) throw e
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (results.length > 0) return results
|
|
198
568
|
}
|
|
199
569
|
|
|
200
|
-
// Unqualified name: search all registries (fuzzy)
|
|
201
|
-
const matches:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
570
|
+
// Unqualified name: search all registries (fuzzy) + browser features
|
|
571
|
+
const matches: ResolvedTarget[] = []
|
|
572
|
+
|
|
573
|
+
if (includeNode) {
|
|
574
|
+
for (const registryName of REGISTRY_NAMES) {
|
|
575
|
+
const reg = container[registryName]
|
|
576
|
+
if (!reg) continue
|
|
577
|
+
const found = fuzzyFind(reg, target)
|
|
578
|
+
if (found) {
|
|
579
|
+
matches.push({ kind: 'helper', registry: registryName, id: found })
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (includeBrowser) {
|
|
585
|
+
const browserFound = fuzzyFindBrowser(target)
|
|
586
|
+
if (browserFound) {
|
|
587
|
+
// If there's already a node feature with the same id, include both
|
|
588
|
+
matches.push({ kind: 'browser-helper', id: browserFound })
|
|
208
589
|
}
|
|
209
590
|
}
|
|
210
591
|
|
|
211
592
|
if (matches.length === 0) {
|
|
212
593
|
const lines = [`"${target}" was not found in any registry.`, '', 'Available:']
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
594
|
+
if (includeNode) {
|
|
595
|
+
for (const registryName of REGISTRY_NAMES) {
|
|
596
|
+
const reg = container[registryName]
|
|
597
|
+
if (reg && reg.available.length > 0) {
|
|
598
|
+
lines.push(` ${registryName}: ${reg.available.join(', ')}`)
|
|
599
|
+
}
|
|
217
600
|
}
|
|
218
601
|
}
|
|
602
|
+
if (includeBrowser && _browserData && _browserData.available.length > 0) {
|
|
603
|
+
lines.push(` browser features: ${_browserData.available.join(', ')}`)
|
|
604
|
+
}
|
|
219
605
|
throw new DescribeError(lines.join('\n'))
|
|
220
606
|
}
|
|
221
607
|
|
|
222
|
-
|
|
608
|
+
// For unqualified names with a single node match and no browser match (or vice versa), return as-is
|
|
609
|
+
// For ambiguous node matches (multiple registries), report ambiguity
|
|
610
|
+
const nodeMatches = matches.filter(m => m.kind === 'helper')
|
|
611
|
+
if (nodeMatches.length > 1) {
|
|
223
612
|
const lines = [`"${target}" is ambiguous — found in multiple registries:`]
|
|
224
|
-
for (const m of
|
|
225
|
-
lines.push(` ${m.registry}.${m.id}`)
|
|
613
|
+
for (const m of nodeMatches) {
|
|
614
|
+
if (m.kind === 'helper') lines.push(` ${m.registry}.${m.id}`)
|
|
226
615
|
}
|
|
227
|
-
lines.push('', `Please qualify it, e.g.: ${
|
|
616
|
+
lines.push('', `Please qualify it, e.g.: ${(nodeMatches[0] as any).registry}.${target}`)
|
|
228
617
|
throw new DescribeError(lines.join('\n'))
|
|
229
618
|
}
|
|
230
619
|
|
|
231
|
-
return
|
|
620
|
+
return matches
|
|
232
621
|
}
|
|
233
622
|
|
|
234
623
|
/** Collect all requested sections from flags. Empty array = show everything. */
|
|
@@ -450,85 +839,135 @@ function findIntermediateParent(Ctor: any, baseClass: any): { name: string; meth
|
|
|
450
839
|
}
|
|
451
840
|
}
|
|
452
841
|
|
|
453
|
-
function getRegistryData(container: any, registryName: RegistryName, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
|
|
842
|
+
function getRegistryData(container: any, registryName: RegistryName, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1, platform: Platform = 'all'): { json: any; text: string } {
|
|
843
|
+
const includeNode = shouldIncludeNode(platform)
|
|
844
|
+
const includeBrowser = shouldIncludeBrowser(platform) && registryName === 'features' && _browserData
|
|
845
|
+
|
|
454
846
|
const registry = container[registryName]
|
|
455
|
-
const
|
|
847
|
+
const nodeAvailable: string[] = includeNode ? registry.available : []
|
|
848
|
+
const browserAvailable: string[] = includeBrowser ? _browserData!.available : []
|
|
456
849
|
|
|
457
|
-
|
|
850
|
+
// Deduplicate: for --platform all, colliding features appear in both lists
|
|
851
|
+
const collidingIds = includeBrowser ? _browserData!.collidingIds : new Set<string>()
|
|
852
|
+
|
|
853
|
+
const totalCount = nodeAvailable.length + browserAvailable.filter(id => !includeNode || !collidingIds.has(id)).length
|
|
854
|
+
|
|
855
|
+
if (totalCount === 0) {
|
|
458
856
|
return { json: {}, text: `No ${registryName} are registered.` }
|
|
459
857
|
}
|
|
460
858
|
|
|
461
|
-
// When no section filters are specified, render a concise index
|
|
462
|
-
// rather than full introspection for every single helper
|
|
859
|
+
// When no section filters are specified, render a concise index
|
|
463
860
|
if (sections.length === 0) {
|
|
464
861
|
const h = '#'.repeat(headingDepth)
|
|
465
862
|
const hSub = '#'.repeat(headingDepth + 1)
|
|
466
863
|
const jsonResult: Record<string, any> = {}
|
|
467
|
-
const textParts: string[] = [`${h} Available ${registryName} (${
|
|
864
|
+
const textParts: string[] = [`${h} Available ${registryName} (${totalCount})\n`]
|
|
468
865
|
|
|
469
866
|
// Show shared methods/getters from the base class at the top
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
867
|
+
if (includeNode) {
|
|
868
|
+
const baseClass = registry.baseClass
|
|
869
|
+
if (baseClass) {
|
|
870
|
+
const shared = collectSharedMembers(baseClass)
|
|
871
|
+
const label = registryName === 'features' ? 'Feature'
|
|
872
|
+
: registryName === 'clients' ? 'Client'
|
|
873
|
+
: registryName === 'servers' ? 'Server'
|
|
874
|
+
: registryName[0]!.toUpperCase() + registryName.slice(1).replace(/s$/, '')
|
|
875
|
+
|
|
876
|
+
if (shared.getters.length) {
|
|
877
|
+
textParts.push(`**Shared ${label} Getters:** ${shared.getters.join(', ')}\n`)
|
|
878
|
+
}
|
|
879
|
+
if (shared.methods.length) {
|
|
880
|
+
textParts.push(`**Shared ${label} Methods:** ${shared.methods.map(m => m + '()').join(', ')}\n`)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
jsonResult._shared = { methods: shared.methods, getters: shared.getters }
|
|
483
884
|
}
|
|
885
|
+
}
|
|
484
886
|
|
|
485
|
-
|
|
887
|
+
// Render node features
|
|
888
|
+
if (includeNode) {
|
|
889
|
+
const baseClass = registry.baseClass
|
|
890
|
+
const sorted = [...nodeAvailable].sort((a, b) => {
|
|
891
|
+
const aCtor = registry.lookup(a)
|
|
892
|
+
const bCtor = registry.lookup(b)
|
|
893
|
+
const aIsDirect = !findIntermediateParent(aCtor, baseClass)
|
|
894
|
+
const bIsDirect = !findIntermediateParent(bCtor, baseClass)
|
|
895
|
+
if (aIsDirect && !bIsDirect) return -1
|
|
896
|
+
if (!aIsDirect && bIsDirect) return 1
|
|
897
|
+
return 0
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
for (const id of sorted) {
|
|
901
|
+
const Ctor = registry.lookup(id)
|
|
902
|
+
const introspection = Ctor.introspect?.()
|
|
903
|
+
const description = introspection?.description || Ctor.description || 'No description provided'
|
|
904
|
+
const summary = extractSummary(description)
|
|
905
|
+
const featureGetters = Object.keys(introspection?.getters || {}).sort()
|
|
906
|
+
const featureMethods = Object.keys(introspection?.methods || {}).sort()
|
|
907
|
+
const intermediate = findIntermediateParent(Ctor, baseClass)
|
|
908
|
+
|
|
909
|
+
// Tag colliding features with (node) when showing both platforms
|
|
910
|
+
const platformTag = includeBrowser && collidingIds.has(id) ? ' (node)' : ''
|
|
911
|
+
|
|
912
|
+
const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
|
|
913
|
+
if (intermediate) {
|
|
914
|
+
entryJson.extends = intermediate.name
|
|
915
|
+
entryJson.inheritedMethods = intermediate.methods
|
|
916
|
+
entryJson.inheritedGetters = intermediate.getters
|
|
917
|
+
}
|
|
918
|
+
if (platformTag) entryJson.platform = 'node'
|
|
919
|
+
jsonResult[id + (platformTag ? ':node' : '')] = entryJson
|
|
920
|
+
|
|
921
|
+
const extendsLine = intermediate ? `\n> extends ${intermediate.name}\n` : ''
|
|
922
|
+
const memberLines: string[] = []
|
|
923
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
924
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
925
|
+
if (intermediate) {
|
|
926
|
+
if (intermediate.getters.length) memberLines.push(` inherited getters: ${intermediate.getters.join(', ')}`)
|
|
927
|
+
if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
|
|
928
|
+
}
|
|
929
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
930
|
+
textParts.push(`${hSub} ${id}${platformTag}${extendsLine}\n${summary}\n${memberBlock}`)
|
|
931
|
+
}
|
|
486
932
|
}
|
|
487
933
|
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
934
|
+
// Render browser features
|
|
935
|
+
if (includeBrowser) {
|
|
936
|
+
for (const id of browserAvailable.sort()) {
|
|
937
|
+
// Skip if already shown as a node feature and platform is not specifically browser
|
|
938
|
+
if (includeNode && collidingIds.has(id)) {
|
|
939
|
+
// Show the browser version too, tagged
|
|
940
|
+
const data = _browserData!.introspection.get(`features.${id}`)
|
|
941
|
+
if (!data) continue
|
|
942
|
+
const summary = extractSummary(data.description || 'No description provided')
|
|
943
|
+
const featureGetters = Object.keys(data.getters || {}).sort()
|
|
944
|
+
const featureMethods = Object.keys(data.methods || {}).sort()
|
|
945
|
+
|
|
946
|
+
jsonResult[id + ':browser'] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
|
|
947
|
+
|
|
948
|
+
const memberLines: string[] = []
|
|
949
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
950
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
951
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
952
|
+
textParts.push(`${hSub} ${id} (browser)\n${summary}\n${memberBlock}`)
|
|
953
|
+
continue
|
|
954
|
+
}
|
|
498
955
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const summary = extractSummary(description)
|
|
505
|
-
|
|
506
|
-
const featureGetters = Object.keys(introspection?.getters || {}).sort()
|
|
507
|
-
const featureMethods = Object.keys(introspection?.methods || {}).sort()
|
|
508
|
-
|
|
509
|
-
// Detect intermediate parent class (e.g. RestClient between ElevenLabsClient and Client)
|
|
510
|
-
const intermediate = findIntermediateParent(Ctor, baseClass)
|
|
511
|
-
|
|
512
|
-
const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
|
|
513
|
-
if (intermediate) {
|
|
514
|
-
entryJson.extends = intermediate.name
|
|
515
|
-
entryJson.inheritedMethods = intermediate.methods
|
|
516
|
-
entryJson.inheritedGetters = intermediate.getters
|
|
517
|
-
}
|
|
518
|
-
jsonResult[id] = entryJson
|
|
956
|
+
const data = _browserData!.introspection.get(`features.${id}`)
|
|
957
|
+
if (!data) continue
|
|
958
|
+
const summary = extractSummary(data.description || 'No description provided')
|
|
959
|
+
const featureGetters = Object.keys(data.getters || {}).sort()
|
|
960
|
+
const featureMethods = Object.keys(data.methods || {}).sort()
|
|
519
961
|
|
|
520
|
-
|
|
962
|
+
const platformTag = !includeNode ? '' : ' (browser)'
|
|
963
|
+
jsonResult[id] = { description: summary, methods: featureMethods, getters: featureGetters, platform: 'browser' }
|
|
521
964
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
|
|
965
|
+
const memberLines: string[] = []
|
|
966
|
+
if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
|
|
967
|
+
if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
|
|
968
|
+
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
969
|
+
textParts.push(`${hSub} ${id}${platformTag}\n${summary}\n${memberBlock}`)
|
|
528
970
|
}
|
|
529
|
-
|
|
530
|
-
const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
|
|
531
|
-
textParts.push(`${hSub} ${id}${extendsLine}\n${summary}\n${memberBlock}`)
|
|
532
971
|
}
|
|
533
972
|
|
|
534
973
|
return { json: jsonResult, text: textParts.join('\n') }
|
|
@@ -537,10 +976,23 @@ function getRegistryData(container: any, registryName: RegistryName, sections: (
|
|
|
537
976
|
// When specific sections are requested, render full detail for each helper
|
|
538
977
|
const jsonResult: Record<string, any> = {}
|
|
539
978
|
const textParts: string[] = []
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
979
|
+
|
|
980
|
+
if (includeNode) {
|
|
981
|
+
for (const id of nodeAvailable) {
|
|
982
|
+
const Ctor = registry.lookup(id)
|
|
983
|
+
jsonResult[id] = renderHelperJson(Ctor, sections, noTitle)
|
|
984
|
+
textParts.push(renderHelperText(Ctor, sections, noTitle, headingDepth))
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (includeBrowser) {
|
|
989
|
+
for (const id of browserAvailable) {
|
|
990
|
+
if (includeNode && collidingIds.has(id)) continue // already shown
|
|
991
|
+
const data = _browserData!.introspection.get(`features.${id}`)
|
|
992
|
+
if (!data) continue
|
|
993
|
+
jsonResult[id] = renderBrowserHelperJson(data, sections, noTitle)
|
|
994
|
+
textParts.push(renderBrowserHelperText(data, sections, noTitle, headingDepth))
|
|
995
|
+
}
|
|
544
996
|
}
|
|
545
997
|
|
|
546
998
|
return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
|
|
@@ -688,6 +1140,13 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
|
|
|
688
1140
|
|
|
689
1141
|
await container.helpers.discoverAll()
|
|
690
1142
|
|
|
1143
|
+
const platform = normalizePlatform(options.platform)
|
|
1144
|
+
|
|
1145
|
+
// Load browser features if needed
|
|
1146
|
+
if (shouldIncludeBrowser(platform)) {
|
|
1147
|
+
await loadBrowserFeatures()
|
|
1148
|
+
}
|
|
1149
|
+
|
|
691
1150
|
const args = container.argv._ as string[]
|
|
692
1151
|
// args[0] is "describe", the rest are targets
|
|
693
1152
|
const targets = args.slice(1)
|
|
@@ -718,7 +1177,7 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
|
|
|
718
1177
|
|
|
719
1178
|
for (const target of targets) {
|
|
720
1179
|
try {
|
|
721
|
-
resolved.push(resolveTarget(target, container))
|
|
1180
|
+
resolved.push(...resolveTarget(target, container, platform))
|
|
722
1181
|
} catch (err: any) {
|
|
723
1182
|
if (err instanceof DescribeError) {
|
|
724
1183
|
console.error(err.message)
|
|
@@ -737,11 +1196,15 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
|
|
|
737
1196
|
case 'container':
|
|
738
1197
|
return getContainerData(container, sections, noTitle, headingDepth)
|
|
739
1198
|
case 'registry':
|
|
740
|
-
return getRegistryData(container, item.name, sections, noTitle, headingDepth)
|
|
1199
|
+
return getRegistryData(container, item.name, sections, noTitle, headingDepth, platform)
|
|
741
1200
|
case 'helper':
|
|
742
1201
|
return getHelperData(container, item.registry, item.id, sections, noTitle, headingDepth)
|
|
743
1202
|
case 'member':
|
|
744
1203
|
return getMemberData(container, item.registry, item.id, item.member, item.memberType, headingDepth)
|
|
1204
|
+
case 'browser-helper':
|
|
1205
|
+
return getBrowserHelperData(item.id, sections, noTitle, headingDepth)
|
|
1206
|
+
case 'browser-member':
|
|
1207
|
+
return getBrowserMemberData(item.id, item.member, item.memberType, headingDepth)
|
|
745
1208
|
}
|
|
746
1209
|
}
|
|
747
1210
|
|